model-api 0.8.7 → 0.8.8

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: 87c1e48361794afbec466d37b263424debab8f5b
4
- data.tar.gz: 53df6228739bf03d9047d22c8d76804193a541ab
3
+ metadata.gz: ff188f893e1fb773d384adfa95ff774a4891eeb4
4
+ data.tar.gz: cc823f6fb228c8d0a66efcfc0997cd3b7623feba
5
5
  SHA512:
6
- metadata.gz: af7384ecb85455e7514fc1ad27e2ea98037730bb6f2995fe0f72840231dc30cfe4056893d7d746a40df6c09ef8fe74d0036b8b6ebcbf3de0e9ad1a6b98ea3f5a
7
- data.tar.gz: 3765d125af6fc44acf91f1ddeada3a937d19c738e35de003e659eeade086586de7204ec136a46d522c56e674feba687f863e195cb8f2739c2f88ce1e8ee95e27
6
+ metadata.gz: ae2c38e837259c246204d3d350fa771b68a132de7c15f9a88c8a3247fdc69d77620c5756f793e0b75716849a8c8ebab54ca8d7a17a3dd291d4fa6aa555d46ac2
7
+ data.tar.gz: 225892b57a109cc7fc71657fb6fba1c9a9cedca877dfa01b2222498fa36476e34a4e93afd33cc4c9e6d7dbacd383fa1bda571f14e2d920948b1c2f8b17563d46
@@ -0,0 +1,444 @@
1
+ module ModelApi
2
+ class ApiContext
3
+ def initialize(context_parent)
4
+ @context_parent = context_parent
5
+ end
6
+
7
+ def model_class
8
+ return @model_class if instance_variable_defined?(:@model_class)
9
+ @model_class = @context_parent.send(:model_class)
10
+ end
11
+
12
+ def prepare_options(opts)
13
+ return opts if opts[:options_initialized]
14
+ if @context_parent.respond_to?(:prepare_options, true)
15
+ return @context_parent.send(:prepare_options, opts)
16
+ end
17
+ opts
18
+ end
19
+
20
+ def api_query(klass, opts = {})
21
+ opts = prepare_options(opts)
22
+ model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
23
+ unless klass < ActiveRecord::Base
24
+ fail 'Expected model class to be an ActiveRecord::Base subclass'
25
+ end
26
+ query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
27
+ if (deleted_col = klass.columns_hash['deleted']).present?
28
+ case deleted_col.type
29
+ when :boolean
30
+ query = query.where(deleted: false)
31
+ when :integer, :decimal
32
+ query = query.where(deleted: 0)
33
+ end
34
+ end
35
+ apply_context(query, opts)
36
+ end
37
+
38
+ def common_object_query(id_attribute, id_value, opts = {})
39
+ klass = opts[:model_class] || model_class
40
+ coll_query = apply_context(api_query(klass, opts), opts)
41
+ query = coll_query.where(id_attribute => id_value)
42
+ if !opts[:admin_user]
43
+ unless opts.include?(:user_filter) && !opts[:user_filter] && opts[:user]
44
+ query = user_query(query, opts[:user], opts.merge(model_class: klass))
45
+ end
46
+ elsif id_attribute != :id && !id_attribute.to_s.ends_with?('.id') &&
47
+ klass.column_names.include?('id') && !query.exists?
48
+ # Admins can optionally use record ID's if the ID field happens to be something else
49
+ query = coll_query.where(id: id_value)
50
+ end
51
+ unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
52
+ not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
53
+ if not_found_error == true
54
+ not_found_error = "#{klass.model_name.human} '#{id_value}' not found."
55
+ end
56
+ fail ModelApi::NotFoundException.new(opts[:id_param] || id_attribute, not_found_error.to_s)
57
+ end
58
+ query
59
+ end
60
+
61
+ def user_query(query, user, opts = {})
62
+ klass = opts[:model_class] || query.klass
63
+ user_id_col = opts[:user_id_column] || :user_id
64
+ user_assoc = opts[:user_association] || :user
65
+ user_id = user.try(opts[:user_id_attribute] || :id)
66
+ if klass.columns_hash.include?(user_id_col.to_s)
67
+ query = query.where(user_id_col => user_id)
68
+ elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
69
+ [:belongs_to, :has_one].include?(assoc.macro)
70
+ query = query.joins(user_assoc).where(
71
+ "#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
72
+ elsif opts[:user_filter]
73
+ fail "Unable to filter results by user; no '#{user_id_col}' column or " \
74
+ "'#{user_assoc}' association found!"
75
+ end
76
+ query
77
+ end
78
+
79
+ def validate_read_operation(obj, operation, opts = {})
80
+ opts = prepare_options(opts)
81
+ status, errors = ModelApi::Utils.validate_operation(obj, operation,
82
+ opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
83
+ return true if status.nil? && errors.nil?
84
+ if errors.nil? && (status.is_a?(Array) || status.present?)
85
+ return true if (errors = status).blank?
86
+ status = :bad_request
87
+ end
88
+ return true unless errors.present?
89
+ errors = [errors] unless errors.is_a?(Array)
90
+ simple_error(status, errors, opts)
91
+ false
92
+ end
93
+
94
+ def get_updated_object(obj_or_class, operation, request_body, opts = {})
95
+ opts = prepare_options(opts.symbolize_keys)
96
+ opts[:operation] = operation
97
+ if obj_or_class.is_a?(Class)
98
+ klass = class_or_sti_subclass(obj_or_class, request_body, operation, opts)
99
+ obj = nil
100
+ elsif obj_or_class.is_a?(ActiveRecord::Base)
101
+ obj = obj_or_class
102
+ klass = obj.class
103
+ elsif obj_or_class.is_a?(ActiveRecord::Relation)
104
+ klass = obj_or_class.klass
105
+ obj = obj_or_class.first
106
+ end
107
+ opts[:api_attr_metadata] = ModelApi::Utils.filtered_attrs(klass, operation, opts)
108
+ opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
109
+ opts[:ignored_fields] = []
110
+ return [nil, opts.merge(bad_payload: true)] if request_body.nil?
111
+ obj = klass.new if obj.nil?
112
+ verify_update_request_body(request_body, opts[:format], opts)
113
+ root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
114
+ request_obj = opts[:request_obj] = object_from_req_body(root_elem, request_body,
115
+ opts[:format])
116
+ opts[:request_hash] = ModelApi::Utils.internal_value(request_obj).deep_symbolize_keys
117
+ ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
118
+ ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
119
+ [obj, opts]
120
+ end
121
+
122
+ def filter_collection(collection, filter_params, opts = {})
123
+ return [collection, {}] if filter_params.blank? # Don't filter if no filter params
124
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
125
+ assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
126
+ result_filters = {}
127
+ metadata.values.each do |attr_metadata|
128
+ collection = apply_filter_param(attr_metadata, collection,
129
+ opts.merge(attr_values: attr_values, result_filters: result_filters, class: klass))
130
+ end
131
+ assoc_values.each do |assoc, assoc_filter_params|
132
+ ar_assoc = klass.reflect_on_association(assoc)
133
+ next unless ar_assoc.present?
134
+ collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
135
+ collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
136
+ opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
137
+ result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
138
+ end
139
+ [collection, result_filters]
140
+ end
141
+
142
+ def process_filter_params(filter_params, klass, opts = {})
143
+ assoc_values = {}
144
+ filter_metadata = {}
145
+ attr_values = {}
146
+ metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
147
+ filter_params.each do |attr, value|
148
+ attr = attr.to_s
149
+ if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
150
+ value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
151
+ attr = attr[0..-2].strip
152
+ end
153
+ if attr.include?('.')
154
+ process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
155
+ else
156
+ process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
157
+ end
158
+ end
159
+ [assoc_values, filter_metadata, attr_values]
160
+ end
161
+
162
+ # rubocop:disable Metrics/ParameterLists
163
+ def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
164
+ attr_elems = attr.split('.')
165
+ assoc_name = attr_elems[0].strip.to_sym
166
+ assoc_metadata = metadata[assoc_name] ||
167
+ metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
168
+ key = assoc_metadata[:key]
169
+ return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
170
+ assoc_filter_params = (assoc_values[key] ||= {})
171
+ assoc_filter_params[attr_elems[1..-1].join('.')] = value
172
+ end
173
+
174
+ def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
175
+ attr = attr.strip.to_sym
176
+ attr_metadata = metadata[attr] ||
177
+ metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
178
+ key = attr_metadata[:key]
179
+ return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
180
+ filter_metadata[key] = attr_metadata
181
+ attr_values[key] = value
182
+ end
183
+
184
+ # rubocop:enable Metrics/ParameterLists
185
+
186
+ def apply_filter_param(attr_metadata, collection, opts = {})
187
+ raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
188
+ filter_table = opts[:filter_table]
189
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
190
+ if raw_value.is_a?(Hash) && raw_value.include?('0')
191
+ operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
192
+ opts)
193
+ else
194
+ operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
195
+ end
196
+ if (column = resolve_key_to_column(klass, attr_metadata)).present?
197
+ operator_value_pairs.each do |operator, value|
198
+ if operator == '=' && filter_table.blank?
199
+ collection = collection.where(column => value)
200
+ else
201
+ table_name = (filter_table || klass.table_name).to_s.delete('`')
202
+ column = column.to_s.delete('`')
203
+ if value.is_a?(Array)
204
+ operator = 'IN'
205
+ value = value.map { |_v| format_value_for_query(column, value, klass) }
206
+ value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
207
+ else
208
+ value = "'#{value.gsub("'", "''")}'"
209
+ end
210
+ collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
211
+ end
212
+ end
213
+ elsif (key = attr_metadata[:key]).present?
214
+ opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
215
+ end
216
+ collection
217
+ end
218
+
219
+ def sort_collection(collection, sort_params, opts = {})
220
+ return [collection, {}] if sort_params.blank? # Don't filter if no filter params
221
+ klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
222
+ assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
223
+ opts.merge(result_sorts: result_sorts))
224
+ sort_table = opts[:sort_table]
225
+ sort_table = sort_table.to_s.delete('`') if sort_table.present?
226
+ attr_sorts.each do |key, sort_order|
227
+ if sort_table.present?
228
+ collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
229
+ "#{sort_order.to_s.upcase}")
230
+ else
231
+ collection = collection.order(key => sort_order)
232
+ end
233
+ end
234
+ assoc_sorts.each do |assoc, assoc_sort_params|
235
+ ar_assoc = klass.reflect_on_association(assoc)
236
+ next unless ar_assoc.present?
237
+ collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
238
+ collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
239
+ opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
240
+ result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
241
+ end
242
+ [collection, result_sorts]
243
+ end
244
+
245
+ private
246
+
247
+ def apply_context(query, opts = {})
248
+ context = opts[:context]
249
+ return query if context.nil?
250
+ if context.respond_to?(:call)
251
+ query = context.send(*([:call, query, opts][0..context.parameters.size]))
252
+ elsif context.is_a?(Hash)
253
+ context.each { |attr, value| query = query.where(attr => value) }
254
+ end
255
+ query
256
+ end
257
+
258
+ def process_sort_params(sort_params, klass, opts)
259
+ metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
260
+ assoc_sorts = {}
261
+ attr_sorts = {}
262
+ result_sorts = {}
263
+ sort_params.each do |attr, sort_order|
264
+ if attr.include?('.')
265
+ process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
266
+ else
267
+ attr = attr.strip.to_sym
268
+ attr_metadata = metadata[attr] || {}
269
+ next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
270
+ sort_order = sort_order.to_sym
271
+ sort_order = :default unless [:asc, :desc].include?(sort_order)
272
+ if sort_order == :default
273
+ sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
274
+ sort_order = :asc unless [:asc, :desc].include?(sort_order)
275
+ end
276
+ if (column = resolve_key_to_column(klass, attr_metadata)).present?
277
+ attr_sorts[column] = sort_order
278
+ elsif (key = attr_metadata[:key]).present?
279
+ result_sorts[key] = sort_order
280
+ end
281
+ end
282
+ end
283
+ [assoc_sorts, attr_sorts, result_sorts]
284
+ end
285
+
286
+ # Intentionally disabling parameter list length check for private / internal method
287
+ # rubocop:disable Metrics/ParameterLists
288
+ def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
289
+ attr_elems = attr.split('.')
290
+ assoc_name = attr_elems[0].strip.to_sym
291
+ assoc_metadata = metadata[assoc_name] || {}
292
+ key = assoc_metadata[:key]
293
+ return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
294
+ assoc_sort_params = (assoc_sorts[key] ||= {})
295
+ assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
296
+ end
297
+
298
+ # rubocop:enable Metrics/ParameterLists
299
+
300
+ def filter_process_param(raw_value, attr_metadata, opts)
301
+ raw_value = raw_value.to_s.strip
302
+ array = nil
303
+ if raw_value.starts_with?('[') && raw_value.ends_with?(']')
304
+ array = JSON.parse(raw_value) rescue nil
305
+ array = array.is_a?(Array) ? array.map(&:to_s) : nil
306
+ end
307
+ if array.nil?
308
+ if attr_metadata.include?(:filter_delimiter)
309
+ delimiter = attr_metadata[:filter_delimiter]
310
+ else
311
+ delimiter = ','
312
+ end
313
+ array = raw_value.split(delimiter) if raw_value.include?(delimiter)
314
+ end
315
+ return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
316
+ operator, value = parse_filter_operator(raw_value)
317
+ [[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
318
+ end
319
+
320
+ def filter_process_param_array(array, attr_metadata, opts)
321
+ operator_value_pairs = []
322
+ equals_values = []
323
+ array.map(&:strip).reject(&:blank?).each do |value|
324
+ operator, value = parse_filter_operator(value)
325
+ value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
326
+ if operator == '='
327
+ equals_values << value
328
+ else
329
+ operator_value_pairs << [operator, value]
330
+ end
331
+ end
332
+ operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
333
+ operator_value_pairs
334
+ end
335
+
336
+ def parse_filter_operator(value)
337
+ value = value.to_s.strip
338
+ if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
339
+ return (operator == '<>' ? '!=' : operator), value[2..-1].strip
340
+ elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
341
+ return operator, value[1..-1].strip
342
+ end
343
+ ['=', value]
344
+ end
345
+
346
+ def format_value_for_query(column, value, klass)
347
+ return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
348
+ column_metadata = klass.columns_hash[column.to_s]
349
+ case column_metadata.try(:type)
350
+ when :date, :datetime, :time, :timestamp
351
+ user = current_user
352
+ if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
353
+ time_zone = ActiveSupport::TimeZone.new(user_time_zone)
354
+ end
355
+ time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
356
+ return time_zone.parse(value.to_s).try(:to_s, :db)
357
+ when :float, :decimal
358
+ return value.to_d.to_s
359
+ when :integer, :primary_key
360
+ return value.to_d.to_s.sub(/\.0\Z/, '')
361
+ when :boolean
362
+ return value ? 'true' : 'false'
363
+ end
364
+ value.to_s
365
+ end
366
+
367
+ def params_array(raw_value)
368
+ index = 0
369
+ array = []
370
+ while raw_value.include?(index.to_s)
371
+ array << raw_value[index.to_s]
372
+ index += 1
373
+ end
374
+ array
375
+ end
376
+
377
+ def resolve_key_to_column(klass, attr_metadata)
378
+ return nil unless klass.respond_to?(:columns_hash)
379
+ columns_hash = klass.columns_hash
380
+ key = attr_metadata[:key]
381
+ return key if columns_hash.include?(key.to_s)
382
+ render_method = attr_metadata[:render_method]
383
+ render_method = render_method.to_s if render_method.is_a?(Symbol)
384
+ return nil unless render_method.is_a?(String)
385
+ columns_hash.include?(render_method) ? render_method : nil
386
+ end
387
+
388
+ def class_or_sti_subclass(klass, req_body, operation, opts = {})
389
+ metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
390
+ if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
391
+ req_body.is_a?(Hash)
392
+ external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
393
+ type = req_body[external_attr.to_s]
394
+ begin
395
+ type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
396
+ rescue Exception => e
397
+ Rails.logger.warn 'Error encountered parsing API input for attribute ' \
398
+ "\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
399
+ 'using raw value instead.'
400
+ end
401
+ if type.present? && (type = type.camelize) != klass.name
402
+ Rails.application.eager_load!
403
+ klass.subclasses.each do |subclass|
404
+ return subclass if subclass.name == type
405
+ end
406
+ end
407
+ end
408
+ klass
409
+ end
410
+
411
+ def object_from_req_body(root_elem, req_body, format)
412
+ if format == :json
413
+ request_obj = req_body
414
+ else
415
+ request_obj = req_body[root_elem]
416
+ if request_obj.blank?
417
+ request_obj = req_body['obj']
418
+ if request_obj.blank? && req_body.size == 1
419
+ request_obj = req_body.values.first
420
+ end
421
+ end
422
+ end
423
+ fail 'Invalid request format' unless request_obj.present?
424
+ request_obj
425
+ end
426
+
427
+ def add_pagination_links(collection_links, coll_route, page, last_page)
428
+ if page < last_page
429
+ collection_links[:next] = [coll_route, { page: (page + 1) }]
430
+ end
431
+ collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
432
+ collection_links[:first] = [coll_route, { page: 1 }]
433
+ collection_links[:last] = [coll_route, { page: last_page }]
434
+ end
435
+
436
+ def verify_update_request_body(request_body, format, opts = {})
437
+ if request_body.is_a?(Array)
438
+ fail 'Expected object, but collection provided'
439
+ elsif !request_body.is_a?(Hash)
440
+ fail 'Expected object'
441
+ end
442
+ end
443
+ end
444
+ end
@@ -1,3 +1,5 @@
1
+ require 'model-api/api_context'
2
+
1
3
  module ModelApi
2
4
  module BaseController
3
5
  module ClassMethods
@@ -39,20 +41,25 @@ module ModelApi
39
41
  self.class.model_class
40
42
  end
41
43
 
44
+ def api_context
45
+ @api_context ||= ModelApi::ApiContext.new(self)
46
+ end
47
+
42
48
  def render_collection(collection, opts = {})
43
49
  return unless ensure_admin_if_admin_only(opts)
44
- opts = prepare_options(opts)
50
+ opts = api_context.prepare_options(opts)
45
51
  opts[:operation] ||= :index
46
- return unless validate_read_operation(collection, opts[:operation], opts)
52
+ return unless api_context.validate_read_operation(collection, opts[:operation], opts)
47
53
 
48
54
  coll_route = opts[:collection_route] || self
49
55
  collection_links = { self: coll_route }
50
56
  collection = ModelApi::Utils.process_collection_includes(collection,
51
57
  opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
52
- collection, _result_filters = filter_collection(collection, find_filter_params, opts)
53
- collection, _result_sorts = sort_collection(collection, find_sort_params, opts)
54
- collection, collection_links, opts = paginate_collection(collection,
55
- collection_links, opts, coll_route)
58
+ collection, _result_filters = api_context.filter_collection(collection, find_filter_params,
59
+ opts)
60
+ collection, _result_sorts = api_context.sort_collection(collection, find_sort_params, opts)
61
+ collection, collection_links, opts = paginate_collection(collection, collection_links, opts,
62
+ coll_route)
56
63
 
57
64
  opts[:collection_links] = collection_links.merge(opts[:collection_links] || {})
58
65
  .reverse_merge(common_response_links(opts))
@@ -62,13 +69,13 @@ module ModelApi
62
69
 
63
70
  def render_object(obj, opts = {})
64
71
  return unless ensure_admin_if_admin_only(opts)
65
- opts = prepare_options(opts)
72
+ opts = api_context.prepare_options(opts)
66
73
  klass = ModelApi::Utils.find_class(obj, opts)
67
74
  object_route = opts[:object_route] || self
68
75
 
69
76
  opts[:object_links] = { self: object_route }
70
77
  if obj.is_a?(ActiveRecord::Base)
71
- return unless validate_read_operation(obj, opts[:operation], opts)
78
+ return unless api_context.validate_read_operation(obj, opts[:operation], opts)
72
79
  unless obj.present?
73
80
  return not_found(opts.merge(class: klass, field: :id))
74
81
  end
@@ -96,12 +103,16 @@ module ModelApi
96
103
  end
97
104
 
98
105
  def prepare_object_for_create(klass, opts = {})
99
- opts = prepare_options(opts)
100
- get_updated_object(klass, get_operation(:create, opts), opts)
106
+ opts = api_context.prepare_options(opts)
107
+ req_body, format = parse_request_body
108
+ opts = add_hateoas_links_for_update(opts)
109
+ api_context.get_updated_object(klass, get_operation(:create, opts), req_body,
110
+ opts.merge(format: format))
111
+
101
112
  end
102
113
 
103
114
  def create_and_render_object(obj, opts = {})
104
- opts = prepare_options(opts)
115
+ opts = api_context.prepare_options(opts)
105
116
  object_link_options = opts[:object_link_options]
106
117
  object_link_options[:action] = :show
107
118
  save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true))
@@ -118,17 +129,20 @@ module ModelApi
118
129
  end
119
130
 
120
131
  def prepare_object_for_update(obj, opts = {})
121
- opts = prepare_options(opts)
122
- get_updated_object(obj, get_operation(:update, opts), opts)
132
+ opts = api_context.prepare_options(opts)
133
+ req_body, format = parse_request_body
134
+ opts = add_hateoas_links_for_update(opts)
135
+ api_context.get_updated_object(obj, get_operation(:update, opts), req_body,
136
+ opts.merge(format: format))
123
137
  end
124
138
 
125
139
  def update_and_render_object(obj, opts = {})
126
- opts = prepare_options(opts)
140
+ opts = api_context.prepare_options(opts)
127
141
  save_and_render_object(obj, get_operation(:update, opts), opts)
128
142
  end
129
143
 
130
144
  def save_and_render_object(obj, operation, opts = {})
131
- opts = prepare_options(opts)
145
+ opts = api_context.prepare_options(opts)
132
146
  status, msgs = Utils.process_updated_model_save(obj, operation, opts)
133
147
  add_hateoas_links_for_updated_object(operation, opts)
134
148
  successful = ModelApi::Utils.response_successful?(status)
@@ -138,7 +152,7 @@ module ModelApi
138
152
 
139
153
  def do_destroy(obj, opts = {})
140
154
  return unless ensure_admin_if_admin_only(opts)
141
- opts = prepare_options(opts)
155
+ opts = api_context.prepare_options(opts)
142
156
  obj = obj.first if obj.is_a?(ActiveRecord::Relation)
143
157
 
144
158
  add_hateoas_links_for_update(opts)
@@ -164,6 +178,7 @@ module ModelApi
164
178
  def initialize_options(opts)
165
179
  return opts if opts[:options_initialized]
166
180
  opts = opts.symbolize_keys
181
+ opts[:api_context] ||= @api_context
167
182
  opts[:model_class] ||= model_class
168
183
  opts[:user] ||= filter_by_user
169
184
  opts[:user_id] ||= opts[:user].try(:id)
@@ -193,81 +208,26 @@ module ModelApi
193
208
  id_info
194
209
  end
195
210
 
196
- def api_query(opts = {})
197
- opts = prepare_options(opts)
198
- klass = opts[:model_class] || model_class
199
- model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
200
- unless klass < ActiveRecord::Base
201
- fail 'Expected model class to be an ActiveRecord::Base subclass'
202
- end
203
- query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
204
- if (deleted_col = klass.columns_hash['deleted']).present?
205
- case deleted_col.type
206
- when :boolean
207
- query = query.where(deleted: false)
208
- when :integer, :decimal
209
- query = query.where(deleted: 0)
210
- end
211
- end
212
- Utils.apply_context(query, opts)
213
- end
214
-
215
211
  def common_object_query(opts = {})
216
- klass = opts[:model_class] || model_class
217
- coll_query = Utils.apply_context(api_query(opts), opts)
212
+ opts = api_context.prepare_options(opts)
218
213
  id_info = opts[:id_info] || id_info(opts)
219
- query = coll_query.where(id_info[:id_attribute] => id_info[:id_value])
220
- if !admin_user?
221
- unless opts.include?(:user_filter) && !opts[:user_filter]
222
- query = user_query(query, opts.merge(model_class: klass))
223
- end
224
- elsif id_info[:id_attribute] != :id && !id_info[:id_attribute].to_s.ends_with?('.id') &&
225
- klass.column_names.include?('id') && !query.exists?
226
- # Admins can optionally use record ID's if the ID field happens to be something else
227
- query = coll_query.where(id: id_info[:id_value])
228
- end
229
- unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
230
- not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
231
- if not_found_error == true
232
- not_found_error = "#{klass.model_name.human} '#{id_info[:id_value]}' not found."
233
- end
234
- fail ModelApi::NotFoundException.new(id_info[:id_param], not_found_error.to_s)
235
- end
236
- query
214
+ api_context.common_object_query(id_info[:id_attribute], id_info[:id_value],
215
+ opts.merge(id_param: id_info[:id_param]))
237
216
  end
238
217
 
239
218
  def collection_query(opts = {})
240
- opts = base_api_options.merge(opts)
219
+ opts = api_context.prepare_options(base_api_options.merge(opts))
241
220
  klass = opts[:model_class] || model_class
242
- query = api_query(opts)
221
+ query = api_context.api_query(klass, opts)
243
222
  unless (opts.include?(:user_filter) && !opts[:user_filter]) ||
244
- (admin? || filtered_by_foreign_key?(query))
245
- query = user_query(query, opts.merge(model_class: klass))
223
+ (admin? || filtered_by_foreign_key?(query)) || !opts[:user]
224
+ query = api_context.user_query(query, opts[:user], opts.merge(model_class: klass))
246
225
  end
247
226
  query
248
227
  end
249
228
 
250
229
  def object_query(opts = {})
251
- common_object_query(base_api_options.merge(opts))
252
- end
253
-
254
- def user_query(query, opts = {})
255
- user = opts[:user] || filter_by_user
256
- klass = opts[:model_class] || query.klass
257
- user_id_col = opts[:user_id_column] || :user_id
258
- user_assoc = opts[:user_association] || :user
259
- user_id = user.try(opts[:user_id_attribute] || :id)
260
- if klass.columns_hash.include?(user_id_col.to_s)
261
- query = query.where(user_id_col => user_id)
262
- elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
263
- [:belongs_to, :has_one].include?(assoc.macro)
264
- query = query.joins(user_assoc).where(
265
- "#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
266
- elsif opts[:user_filter]
267
- fail "Unable to filter results by user; no '#{user_id_col}' column or " \
268
- "'#{user_assoc}' association found!"
269
- end
270
- query
230
+ common_object_query(api_context.prepare_options(base_api_options.merge(opts)))
271
231
  end
272
232
 
273
233
  def base_api_options
@@ -344,6 +304,7 @@ module ModelApi
344
304
  end
345
305
 
346
306
  def resource_parent_id(parent_model_class, opts = {})
307
+ opts = api_context.prepare_options(opts)
347
308
  id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id"))
348
309
  model_name = parent_model_class.model_name.human
349
310
  if id_info[:id_value].blank?
@@ -421,21 +382,6 @@ module ModelApi
421
382
  simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts)
422
383
  end
423
384
 
424
- def validate_read_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]))
428
- return true if status.nil? && errors.nil?
429
- if errors.nil? && (status.is_a?(Array) || status.present?)
430
- return true if (errors = status).blank?
431
- status = :bad_request
432
- end
433
- return true unless errors.present?
434
- errors = [errors] unless errors.is_a?(Array)
435
- simple_error(status, errors, opts)
436
- false
437
- end
438
-
439
385
  def filter_by_user
440
386
  current_user
441
387
  end
@@ -476,32 +422,11 @@ module ModelApi
476
422
  end
477
423
  end
478
424
 
479
- def get_updated_object(obj_or_class, operation, opts = {})
480
- opts = prepare_options(opts.symbolize_keys)
481
- opts[:operation] = operation
482
- req_body, format = ModelApi::Utils.parse_request_body(request)
483
- if obj_or_class.is_a?(Class)
484
- klass = Utils.class_or_sti_subclass(obj_or_class, req_body, operation, opts)
485
- obj = nil
486
- elsif obj_or_class.is_a?(ActiveRecord::Base)
487
- obj = obj_or_class
488
- klass = obj.class
489
- elsif obj_or_class.is_a?(ActiveRecord::Relation)
490
- klass = obj_or_class.klass
491
- obj = obj_or_class.first
425
+ def parse_request_body
426
+ unless instance_variable_defined?(:@request_body)
427
+ @req_body, @format = ModelApi::Utils.parse_request_body(request)
492
428
  end
493
- opts[:api_attr_metadata] = ModelApi::Utils.filtered_attrs(klass, operation, opts)
494
- opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
495
- opts[:ignored_fields] = []
496
- return [nil, opts.merge(bad_payload: true)] if req_body.nil?
497
- obj = klass.new if obj.nil?
498
- add_hateoas_links_for_update(opts)
499
- verify_update_request_body(req_body, format, opts)
500
- root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
501
- request_obj = opts[:request_obj] = Utils.object_from_req_body(root_elem, req_body, format)
502
- ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
503
- ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
504
- [obj, opts]
429
+ [@req_body, @format]
505
430
  end
506
431
 
507
432
  private
@@ -572,248 +497,6 @@ module ModelApi
572
497
  sort_params
573
498
  end
574
499
 
575
- def filter_collection(collection, filter_params, opts = {})
576
- return [collection, {}] if filter_params.blank? # Don't filter if no filter params
577
- klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
578
- assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
579
- result_filters = {}
580
- metadata.values.each do |attr_metadata|
581
- collection = apply_filter_param(attr_metadata, collection,
582
- opts.merge(attr_values: attr_values, result_filters: result_filters, class: klass))
583
- end
584
- assoc_values.each do |assoc, assoc_filter_params|
585
- ar_assoc = klass.reflect_on_association(assoc)
586
- next unless ar_assoc.present?
587
- collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
588
- collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
589
- opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
590
- result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
591
- end
592
- [collection, result_filters]
593
- end
594
-
595
- def process_filter_params(filter_params, klass, opts = {})
596
- assoc_values = {}
597
- filter_metadata = {}
598
- attr_values = {}
599
- metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
600
- filter_params.each do |attr, value|
601
- attr = attr.to_s
602
- if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
603
- value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
604
- attr = attr[0..-2].strip
605
- end
606
- if attr.include?('.')
607
- process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
608
- else
609
- process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
610
- end
611
- end
612
- [assoc_values, filter_metadata, attr_values]
613
- end
614
-
615
- # rubocop:disable Metrics/ParameterLists
616
- def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
617
- attr_elems = attr.split('.')
618
- assoc_name = attr_elems[0].strip.to_sym
619
- assoc_metadata = metadata[assoc_name] ||
620
- metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
621
- key = assoc_metadata[:key]
622
- return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
623
- assoc_filter_params = (assoc_values[key] ||= {})
624
- assoc_filter_params[attr_elems[1..-1].join('.')] = value
625
- end
626
-
627
- def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
628
- attr = attr.strip.to_sym
629
- attr_metadata = metadata[attr] ||
630
- metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
631
- key = attr_metadata[:key]
632
- return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
633
- filter_metadata[key] = attr_metadata
634
- attr_values[key] = value
635
- end
636
-
637
- # rubocop:enable Metrics/ParameterLists
638
-
639
- def apply_filter_param(attr_metadata, collection, opts = {})
640
- raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
641
- filter_table = opts[:filter_table]
642
- klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
643
- if raw_value.is_a?(Hash) && raw_value.include?('0')
644
- operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
645
- opts)
646
- else
647
- operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
648
- end
649
- if (column = resolve_key_to_column(klass, attr_metadata)).present?
650
- operator_value_pairs.each do |operator, value|
651
- if operator == '=' && filter_table.blank?
652
- collection = collection.where(column => value)
653
- else
654
- table_name = (filter_table || klass.table_name).to_s.delete('`')
655
- column = column.to_s.delete('`')
656
- if value.is_a?(Array)
657
- operator = 'IN'
658
- value = value.map { |_v| format_value_for_query(column, value, klass) }
659
- value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
660
- else
661
- value = "'#{value.gsub("'", "''")}'"
662
- end
663
- collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
664
- end
665
- end
666
- elsif (key = attr_metadata[:key]).present?
667
- opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
668
- end
669
- collection
670
- end
671
-
672
- def sort_collection(collection, sort_params, opts = {})
673
- return [collection, {}] if sort_params.blank? # Don't filter if no filter params
674
- klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
675
- assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
676
- opts.merge(result_sorts: result_sorts))
677
- sort_table = opts[:sort_table]
678
- sort_table = sort_table.to_s.delete('`') if sort_table.present?
679
- attr_sorts.each do |key, sort_order|
680
- if sort_table.present?
681
- collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
682
- "#{sort_order.to_s.upcase}")
683
- else
684
- collection = collection.order(key => sort_order)
685
- end
686
- end
687
- assoc_sorts.each do |assoc, assoc_sort_params|
688
- ar_assoc = klass.reflect_on_association(assoc)
689
- next unless ar_assoc.present?
690
- collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
691
- collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
692
- opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
693
- result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
694
- end
695
- [collection, result_sorts]
696
- end
697
-
698
- def process_sort_params(sort_params, klass, opts)
699
- metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
700
- assoc_sorts = {}
701
- attr_sorts = {}
702
- result_sorts = {}
703
- sort_params.each do |attr, sort_order|
704
- if attr.include?('.')
705
- process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
706
- else
707
- attr = attr.strip.to_sym
708
- attr_metadata = metadata[attr] || {}
709
- next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
710
- sort_order = sort_order.to_sym
711
- sort_order = :default unless [:asc, :desc].include?(sort_order)
712
- if sort_order == :default
713
- sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
714
- sort_order = :asc unless [:asc, :desc].include?(sort_order)
715
- end
716
- if (column = resolve_key_to_column(klass, attr_metadata)).present?
717
- attr_sorts[column] = sort_order
718
- elsif (key = attr_metadata[:key]).present?
719
- result_sorts[key] = sort_order
720
- end
721
- end
722
- end
723
- [assoc_sorts, attr_sorts, result_sorts]
724
- end
725
-
726
- # Intentionally disabling parameter list length check for private / internal method
727
- # rubocop:disable Metrics/ParameterLists
728
- def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
729
- attr_elems = attr.split('.')
730
- assoc_name = attr_elems[0].strip.to_sym
731
- assoc_metadata = metadata[assoc_name] || {}
732
- key = assoc_metadata[:key]
733
- return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
734
- assoc_sort_params = (assoc_sorts[key] ||= {})
735
- assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
736
- end
737
-
738
- # rubocop:enable Metrics/ParameterLists
739
-
740
- def filter_process_param(raw_value, attr_metadata, opts)
741
- raw_value = raw_value.to_s.strip
742
- array = nil
743
- if raw_value.starts_with?('[') && raw_value.ends_with?(']')
744
- array = JSON.parse(raw_value) rescue nil
745
- array = array.is_a?(Array) ? array.map(&:to_s) : nil
746
- end
747
- if array.nil?
748
- if attr_metadata.include?(:filter_delimiter)
749
- delimiter = attr_metadata[:filter_delimiter]
750
- else
751
- delimiter = ','
752
- end
753
- array = raw_value.split(delimiter) if raw_value.include?(delimiter)
754
- end
755
- return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
756
- operator, value = parse_filter_operator(raw_value)
757
- [[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
758
- end
759
-
760
- def filter_process_param_array(array, attr_metadata, opts)
761
- operator_value_pairs = []
762
- equals_values = []
763
- array.map(&:strip).reject(&:blank?).each do |value|
764
- operator, value = parse_filter_operator(value)
765
- value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
766
- if operator == '='
767
- equals_values << value
768
- else
769
- operator_value_pairs << [operator, value]
770
- end
771
- end
772
- operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
773
- operator_value_pairs
774
- end
775
-
776
- def parse_filter_operator(value)
777
- value = value.to_s.strip
778
- if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
779
- return (operator == '<>' ? '!=' : operator), value[2..-1].strip
780
- elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
781
- return operator, value[1..-1].strip
782
- end
783
- ['=', value]
784
- end
785
-
786
- def format_value_for_query(column, value, klass)
787
- return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
788
- column_metadata = klass.columns_hash[column.to_s]
789
- case column_metadata.try(:type)
790
- when :date, :datetime, :time, :timestamp
791
- user = current_user
792
- if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
793
- time_zone = ActiveSupport::TimeZone.new(user_time_zone)
794
- end
795
- time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
796
- return time_zone.parse(value.to_s).try(:to_s, :db)
797
- when :float, :decimal
798
- return value.to_d.to_s
799
- when :integer, :primary_key
800
- return value.to_d.to_s.sub(/\.0\Z/, '')
801
- when :boolean
802
- return value ? 'true' : 'false'
803
- end
804
- value.to_s
805
- end
806
-
807
- def params_array(raw_value)
808
- index = 0
809
- array = []
810
- while raw_value.include?(index.to_s)
811
- array << raw_value[index.to_s]
812
- index += 1
813
- end
814
- array
815
- end
816
-
817
500
  def paginate_collection(collection, collection_links, opts, coll_route)
818
501
  collection_size = collection.count
819
502
  page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i
@@ -837,24 +520,13 @@ module ModelApi
837
520
 
838
521
  if collection_size > page_size
839
522
  opts[:collection_link_options][:page] = page
840
- Utils.add_pagination_links(collection_links, coll_route, page, page_count)
523
+ add_pagination_links(collection_links, coll_route, page, page_count)
841
524
  collection = collection.limit(page_size).offset(offset)
842
525
  end
843
526
 
844
527
  [collection, collection_links, opts]
845
528
  end
846
529
 
847
- def resolve_key_to_column(klass, attr_metadata)
848
- return nil unless klass.respond_to?(:columns_hash)
849
- columns_hash = klass.columns_hash
850
- key = attr_metadata[:key]
851
- return key if columns_hash.include?(key.to_s)
852
- render_method = attr_metadata[:render_method]
853
- render_method = render_method.to_s if render_method.is_a?(Symbol)
854
- return nil unless render_method.is_a?(String)
855
- columns_hash.include?(render_method) ? render_method : nil
856
- end
857
-
858
530
  def add_collection_object_route(opts)
859
531
  object_route = opts[:object_route]
860
532
  unless object_route.present?
@@ -877,6 +549,7 @@ module ModelApi
877
549
  object_route = opts[:object_route] || self
878
550
  links = { self: object_route }.reverse_merge(common_response_links(opts))
879
551
  opts[:links] = links.merge(opts[:links] || {})
552
+ opts
880
553
  end
881
554
 
882
555
  def add_hateoas_links_for_updated_object(_operation, opts)
@@ -885,18 +558,6 @@ module ModelApi
885
558
  opts[:object_links] = object_links.merge(opts[:object_links] || {})
886
559
  end
887
560
 
888
- def verify_update_request_body(request_body, format, opts = {})
889
- if request.format.symbol.nil? && format.present?
890
- opts[:format] ||= format
891
- end
892
-
893
- if request_body.is_a?(Array)
894
- fail 'Expected object, but collection provided'
895
- elsif !request_body.is_a?(Hash)
896
- fail 'Expected object'
897
- end
898
- end
899
-
900
561
  def filtered_by_foreign_key?(query)
901
562
  fk_cache = self.class.instance_variable_get(:@foreign_key_cache)
902
563
  self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil?
@@ -916,31 +577,6 @@ module ModelApi
916
577
 
917
578
  class Utils
918
579
  class << self
919
- def add_pagination_links(collection_links, coll_route, page, last_page)
920
- if page < last_page
921
- collection_links[:next] = [coll_route, { page: (page + 1) }]
922
- end
923
- collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
924
- collection_links[:first] = [coll_route, { page: 1 }]
925
- collection_links[:last] = [coll_route, { page: last_page }]
926
- end
927
-
928
- def object_from_req_body(root_elem, req_body, format)
929
- if format == :json
930
- request_obj = req_body
931
- else
932
- request_obj = req_body[root_elem]
933
- if request_obj.blank?
934
- request_obj = req_body['obj']
935
- if request_obj.blank? && req_body.size == 1
936
- request_obj = req_body.values.first
937
- end
938
- end
939
- end
940
- fail 'Invalid request format' unless request_obj.present?
941
- request_obj
942
- end
943
-
944
580
  def process_updated_model_save(obj, operation, opts = {})
945
581
  opts = opts.dup
946
582
  opts[:operation] = operation
@@ -1012,40 +648,6 @@ module ModelApi
1012
648
  Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
1013
649
  false
1014
650
  end
1015
-
1016
- def apply_context(query, opts = {})
1017
- context = opts[:context]
1018
- return query if context.nil?
1019
- if context.respond_to?(:call)
1020
- query = context.send(*([:call, query, opts][0..context.parameters.size]))
1021
- elsif context.is_a?(Hash)
1022
- context.each { |attr, value| query = query.where(attr => value) }
1023
- end
1024
- query
1025
- end
1026
-
1027
- def class_or_sti_subclass(klass, req_body, operation, opts = {})
1028
- metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
1029
- if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
1030
- req_body.is_a?(Hash)
1031
- external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
1032
- type = req_body[external_attr.to_s]
1033
- begin
1034
- type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
1035
- rescue Exception => e
1036
- Rails.logger.warn 'Error encountered parsing API input for attribute ' \
1037
- "\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
1038
- 'using raw value instead.'
1039
- end
1040
- if type.present? && (type = type.camelize) != klass.name
1041
- Rails.application.eager_load!
1042
- klass.subclasses.each do |subclass|
1043
- return subclass if subclass.name == type
1044
- end
1045
- end
1046
- end
1047
- klass
1048
- end
1049
651
  end
1050
652
  end
1051
653
  end
@@ -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.7'
6
+ s.version = '0.8.8'
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.7
4
+ version: 0.8.8
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-10-20 00:00:00.000000000 Z
11
+ date: 2016-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -67,6 +67,7 @@ extra_rdoc_files: []
67
67
  files:
68
68
  - README.md
69
69
  - lib/model-api.rb
70
+ - lib/model-api/api_context.rb
70
71
  - lib/model-api/base_controller.rb
71
72
  - lib/model-api/bypass_parse_middleware.rb
72
73
  - lib/model-api/hash_metadata.rb