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 +4 -4
- data/README.md +48 -8
- data/lib/model-api.rb +2 -2
- data/lib/model-api/base_controller.rb +170 -492
- data/lib/model-api/utils.rb +425 -23
- data/model-api.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95ae1633c2baa4a79b179c84be9ddd6d7b88a90b
|
4
|
+
data.tar.gz: d0c704c3b3da52e804d882b6f2f058990c52204a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
72
|
+
To expose a resource, start by defining the underlying model for that resource:
|
66
73
|
``` ruby
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
```
|
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
@@ -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,
|
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,
|
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
|
163
|
+
|
164
|
+
def initialize_options(opts)
|
165
|
+
return opts if opts[:options_initialized]
|
162
166
|
opts = opts.symbolize_keys
|
163
|
-
opts[:
|
164
|
-
opts[:
|
165
|
-
opts[:
|
166
|
-
opts[:
|
167
|
-
opts[:
|
168
|
-
|
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 !
|
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
|
-
(
|
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] ||
|
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
|
-
|
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
|
305
|
-
|
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
|
310
|
-
|
311
|
-
param
|
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
|
-
|
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.
|
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
|
-
|
979
|
-
|
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
|
-
|
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 =
|
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) &&
|