model-api 0.8.4 → 0.8.5
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 +198 -1
- data/lib/model-api/base_controller.rb +434 -419
- data/lib/model-api/model.rb +20 -7
- data/lib/model-api/open_api_extensions.rb +4 -2
- data/lib/model-api/simple_metadata.rb +3 -1
- data/model-api.gemspec +4 -5
- metadata +13 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ad27c0bbc9961da1c6b1aa4715e05f3f6af9297
|
4
|
+
data.tar.gz: a81ac47433b86382e72f21ae7e12113dabc3fcd5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b99fcc0cffd7e8d24210afab23dea9b8cec64bef71afea263738ca6e4b4169232b7792fa5f2fd33471d610cc4d406f6499724ba06e88f8aacf337e671b98dee
|
7
|
+
data.tar.gz: 823d85c47661c643d443f8bef2403ce22ee3f20f2a53ce6fbfe45630271a0005d76a411569573bf10d3b9dcf655d02a45d2a86b02eda11a0406f8a086ed31871
|
data/README.md
CHANGED
@@ -1,2 +1,199 @@
|
|
1
1
|
# model_api
|
2
|
-
|
2
|
+
Rails REST API's made easy.
|
3
|
+
|
4
|
+
## Why use model-api?
|
5
|
+
Developing REST API's in a conventional manner involves many challenges. Between the parameters,
|
6
|
+
route details, payloads, responses, and API documentation, it's difficult to find an implementation
|
7
|
+
strategy that minimizes repetition and organizes the details in a simple, easy-to-maintain manner.
|
8
|
+
This is where model-api comes in.
|
9
|
+
|
10
|
+
With model-api, you can:
|
11
|
+
* Consolidate your payload and response metadata for a given resource in the ActiveRecord model.
|
12
|
+
* Set up easily-configurable filtering, pagination, sorting, and HATEOAS link generation.
|
13
|
+
* Render, link, create, search upon, and sort upon your resources' associated objects with ease.
|
14
|
+
* Easily leverage the ActiveRecord validation rules present in your application's models to
|
15
|
+
validate calls to your API.
|
16
|
+
* Automatically generate OpenAPI documentation with each deployment that's guaranteed to be in
|
17
|
+
sync with what your API actually supports.
|
18
|
+
* Reduce the lines of code required to implement your Rails-based Rest API dramatically.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Put this in your Gemfile:
|
23
|
+
|
24
|
+
``` ruby
|
25
|
+
gem 'open-api'
|
26
|
+
```
|
27
|
+
|
28
|
+
## Configuration
|
29
|
+
|
30
|
+
The `model-api` gem doesn't require configuration. However, if you plan to generate OpenAPI
|
31
|
+
documentation, add an `open_api.rb` file to `config/initializers` and configure as follows:
|
32
|
+
|
33
|
+
``` ruby
|
34
|
+
OpenApi.configure do |config|
|
35
|
+
|
36
|
+
# Default base path(s), used to scan Rails routes for API endpoints.
|
37
|
+
config.base_paths = ['/widget-api/v1']
|
38
|
+
|
39
|
+
# General information about your API.
|
40
|
+
config.info = {
|
41
|
+
title: 'Acme Widget API',
|
42
|
+
description: "Documentation of the Acme's Widget API service",
|
43
|
+
version: '1.0.0',
|
44
|
+
terms_of_service: 'https://www.acme.com/widget-api/terms_of_service',
|
45
|
+
|
46
|
+
contact: {
|
47
|
+
name: 'Acme Corporation API Team',
|
48
|
+
url: 'http://www.acme.com/widget-api',
|
49
|
+
email: 'widget-api-support@acme.com'
|
50
|
+
},
|
51
|
+
|
52
|
+
license: {
|
53
|
+
name: 'Apache 2.0',
|
54
|
+
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
# Default output file path for your generated Open API JSON document.
|
59
|
+
config.output_file_path = Rails.root.join('apidoc', 'api-docs.json')
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Exposing a Resource
|
64
|
+
|
65
|
+
To expose a resource, start by adding the resource routes to your `routes.rb` file:
|
66
|
+
``` ruby
|
67
|
+
namespace :api do
|
68
|
+
namespace :v1 do
|
69
|
+
resource :books, except: [:new, :edit], param: :book_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Next, define the base controller class that all of your API controllers will extend
|
75
|
+
(for this example, in `app/controllers/api/v1/base_controller.rb`):
|
76
|
+
``` ruby
|
77
|
+
module Api
|
78
|
+
module V1
|
79
|
+
class BaseController < ActionController::Base
|
80
|
+
include ModelApi::BaseController
|
81
|
+
include ModelApi::OpenApiExtensions
|
82
|
+
include OpenApi::Controller
|
83
|
+
|
84
|
+
# OpenAPI documentation metadata shared by all endpoints, including common query string
|
85
|
+
# parameters, HTTP headers, and HTTP response codes.
|
86
|
+
open_api_controller \
|
87
|
+
query_string: {
|
88
|
+
access_token: {
|
89
|
+
type: :string,
|
90
|
+
description: 'OAuth 2 access token query parameter',
|
91
|
+
required: false
|
92
|
+
}
|
93
|
+
},
|
94
|
+
headers: {
|
95
|
+
'Authorization' => {
|
96
|
+
type: :string,
|
97
|
+
description: 'Authorization header (format: "bearer <access token>")',
|
98
|
+
required: false
|
99
|
+
}
|
100
|
+
},
|
101
|
+
responses: {
|
102
|
+
200 => { description: 'Successful' },
|
103
|
+
400 => { description: 'Not found' },
|
104
|
+
401 => { description: 'Invalid request' },
|
105
|
+
403 => { description: 'Not authorized (typically missing / invalid access token)' }
|
106
|
+
}
|
107
|
+
|
108
|
+
# OpenAPI documentation for common API endpoint path parameters
|
109
|
+
open_api_path_param :book_id, description: 'Book identifier'
|
110
|
+
|
111
|
+
# HATEOAS links common to all responses (e.g. a common terms-of-service link)
|
112
|
+
def common_response_links(_opts = {})
|
113
|
+
{ 'terms-of-service' => URI(url_for(controller: '/home', action: :terms_of_service)) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
Finally, add a controller for your new resource (for this example, in
|
121
|
+
`app/controllers/api/v1/base_controller.rb`):
|
122
|
+
```ruby
|
123
|
+
module Api
|
124
|
+
module V1
|
125
|
+
class BooksController < BaseController
|
126
|
+
class << self
|
127
|
+
|
128
|
+
# Default model class to use for API endpoints in this controller
|
129
|
+
def model_class
|
130
|
+
Book
|
131
|
+
end
|
132
|
+
|
133
|
+
# Default options for model-api helper methods used to process requests to endpoints
|
134
|
+
def base_api_options
|
135
|
+
super.merge(id_param: :book_id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# OpenAPI metadata describing the collective set of endpoints defined in this controller
|
140
|
+
open_api_controller \
|
141
|
+
tag: {
|
142
|
+
name: 'Books',
|
143
|
+
description: 'Comprehensive list of available books'
|
144
|
+
}
|
145
|
+
|
146
|
+
# GET /api/v1/books endpoint OpenAPI doc metadata and implementation
|
147
|
+
add_open_api_action :index, :index, base_api_options.merge(
|
148
|
+
description: 'Retrieve list of available books')
|
149
|
+
def index
|
150
|
+
render_collection collection_query, base_api_options
|
151
|
+
end
|
152
|
+
|
153
|
+
# GET /api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
|
154
|
+
add_open_api_action :show, :show, base_api_options.merge(
|
155
|
+
description: 'Retrieve details for a specific book')
|
156
|
+
def show
|
157
|
+
render_object object_query.first, base_api_options
|
158
|
+
end
|
159
|
+
|
160
|
+
# POST /api/v1/books endpoint OpenAPI doc metadata and implementation
|
161
|
+
add_open_api_action :create, :create, base_api_options.merge(
|
162
|
+
description: 'Create a new book')
|
163
|
+
def create
|
164
|
+
do_create base_api_options
|
165
|
+
end
|
166
|
+
|
167
|
+
# PATCH/PUT api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
|
168
|
+
add_open_api_action :update, :update, base_api_options.merge(
|
169
|
+
description: 'Update an existing book')
|
170
|
+
def update
|
171
|
+
do_update object_query, base_api_options
|
172
|
+
end
|
173
|
+
|
174
|
+
# DELETE /api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
|
175
|
+
add_open_api_action :destroy, :destroy, base_api_options.merge(
|
176
|
+
description: 'Delete an existing book')
|
177
|
+
def destroy
|
178
|
+
do_destroy object_query, base_api_options
|
179
|
+
end
|
180
|
+
|
181
|
+
def object_query(opts = {})
|
182
|
+
super(opts.merge(not_found_error: true))
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
## Generating Documentation
|
190
|
+
|
191
|
+
To generate OpenAPI documentation:
|
192
|
+
```
|
193
|
+
rake open_api:docs
|
194
|
+
```
|
195
|
+
|
196
|
+
Optionally, you may specify a base route path and output file:
|
197
|
+
```
|
198
|
+
rake open_api:docs[/api/v1,/home/myhome/api-v1.json]
|
199
|
+
```
|
@@ -13,16 +13,18 @@ module ModelApi
|
|
13
13
|
base_api_options.merge(admin_only: true)
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def included(base)
|
19
|
+
base.extend(ClassMethods)
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
base.send(:rescue_from, Exception, with: :unhandled_exception)
|
25
|
-
base.send(:respond_to, :json, :xml)
|
21
|
+
base.send(:include, InstanceMethods)
|
22
|
+
|
23
|
+
base.send(:before_filter, :common_headers)
|
24
|
+
|
25
|
+
base.send(:rescue_from, Exception, with: :unhandled_exception)
|
26
|
+
base.send(:respond_to, :json, :xml)
|
27
|
+
end
|
26
28
|
end
|
27
29
|
|
28
30
|
module InstanceMethods
|
@@ -236,7 +238,7 @@ module ModelApi
|
|
236
238
|
klass = opts[:model_class] || model_class
|
237
239
|
user_id_col = opts[:user_id_column] || :user_id
|
238
240
|
user_assoc = opts[:user_association] || :user
|
239
|
-
user_id = user.
|
241
|
+
user_id = user.try(opts[:user_id_attribute] || :id)
|
240
242
|
if klass.columns_hash.include?(user_id_col.to_s)
|
241
243
|
query = query.where(user_id_col => user_id)
|
242
244
|
elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
|
@@ -259,8 +261,9 @@ module ModelApi
|
|
259
261
|
end
|
260
262
|
|
261
263
|
def ensure_admin
|
262
|
-
|
263
|
-
|
264
|
+
user = current_user
|
265
|
+
return true if user.respond_to?(:admin_api_user?) && user.admin_api_user?
|
266
|
+
|
264
267
|
# Mask presence of endpoint if user is not authorized to access it
|
265
268
|
not_found
|
266
269
|
false
|
@@ -268,19 +271,14 @@ module ModelApi
|
|
268
271
|
|
269
272
|
def unhandled_exception(err)
|
270
273
|
return if handle_api_exceptions(err)
|
271
|
-
error_id = LogUtils.log_and_notify(err)
|
272
274
|
return if performed?
|
273
275
|
error_details = {}
|
274
276
|
if Rails.env == 'development'
|
275
277
|
error_details[:message] = "Exception: #{err.message}"
|
276
|
-
error_details[:error_event_id] = error_id
|
277
278
|
error_details[:backtrace] = err.backtrace
|
278
279
|
else
|
279
280
|
error_details[:message] = 'An internal server error has occurred ' \
|
280
|
-
'while processing your request.
|
281
|
-
'support, referencing the following error event id, for ' \
|
282
|
-
"assistance: #{error_id}"
|
283
|
-
error_details[:error_event_id] = error_id
|
281
|
+
'while processing your request.'
|
284
282
|
end
|
285
283
|
ModelApi::Renderer.render(self, error_details, root: :error_details,
|
286
284
|
status: :internal_server_error)
|
@@ -403,31 +401,15 @@ module ModelApi
|
|
403
401
|
simple_error(status, errors, opts)
|
404
402
|
false
|
405
403
|
end
|
406
|
-
|
407
|
-
def current_user
|
408
|
-
return @devise_user if @devise_user.present?
|
409
|
-
return @current_user if instance_variable_defined?(:@current_user)
|
410
|
-
unless doorkeeper_token.present? &&
|
411
|
-
doorkeeper_token.resource_owner_id.present?
|
412
|
-
return (@current_user = nil)
|
413
|
-
end
|
414
|
-
@current_user = User.find(doorkeeper_token.resource_owner_id)
|
415
|
-
end
|
416
|
-
|
404
|
+
|
417
405
|
def filter_by_user
|
418
|
-
if admin_access?
|
419
|
-
if (user_id = request.query_parameters[:user_id] ||
|
420
|
-
request.query_parameters[:user]).present?
|
421
|
-
return User.where(id: user_id.to_i).first || current_user
|
422
|
-
elsif (username = request.query_parameters[:username]).present?
|
423
|
-
return User.where(username: username.to_s).first || current_user
|
424
|
-
elsif (user_email = request.query_parameters[:user_email]).present?
|
425
|
-
return User.where(email: user_email.to_s).first || current_user
|
426
|
-
end
|
427
|
-
end
|
428
406
|
current_user
|
429
407
|
end
|
430
408
|
|
409
|
+
def current_user
|
410
|
+
nil
|
411
|
+
end
|
412
|
+
|
431
413
|
def common_headers
|
432
414
|
ModelApi::Utils.common_http_headers.each do |k, v|
|
433
415
|
response.headers[k] = v
|
@@ -791,8 +773,9 @@ module ModelApi
|
|
791
773
|
column_metadata = klass.columns_hash[column.to_s]
|
792
774
|
case column_metadata.try(:type)
|
793
775
|
when :date, :datetime, :time, :timestamp
|
794
|
-
|
795
|
-
|
776
|
+
user = current_user
|
777
|
+
if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
|
778
|
+
time_zone = ActiveSupport::TimeZone.new(user_time_zone)
|
796
779
|
end
|
797
780
|
time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
|
798
781
|
return time_zone.parse(value.to_s).try(:to_s, :db)
|
@@ -917,148 +900,165 @@ module ModelApi
|
|
917
900
|
end
|
918
901
|
|
919
902
|
class Utils
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
def self.add_pagination_links(collection_links, coll_route, page, last_page)
|
926
|
-
if page < last_page
|
927
|
-
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
903
|
+
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)
|
928
907
|
end
|
929
|
-
|
930
|
-
collection_links
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
908
|
+
|
909
|
+
def add_pagination_links(collection_links, coll_route, page, last_page)
|
910
|
+
if page < last_page
|
911
|
+
collection_links[:next] = [coll_route, { page: (page + 1) }]
|
912
|
+
end
|
913
|
+
collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
|
914
|
+
collection_links[:first] = [coll_route, { page: 1 }]
|
915
|
+
collection_links[:last] = [coll_route, { page: last_page }]
|
916
|
+
end
|
917
|
+
|
918
|
+
def object_from_req_body(root_elem, req_body, format)
|
919
|
+
if format == :json
|
920
|
+
request_obj = req_body
|
921
|
+
else
|
922
|
+
request_obj = req_body[root_elem]
|
923
|
+
if request_obj.blank?
|
924
|
+
request_obj = req_body['obj']
|
925
|
+
if request_obj.blank? && req_body.size == 1
|
926
|
+
request_obj = req_body.values.first
|
927
|
+
end
|
943
928
|
end
|
944
929
|
end
|
930
|
+
fail 'Invalid request format' unless request_obj.present?
|
931
|
+
request_obj
|
945
932
|
end
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
next
|
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))
|
961
947
|
end
|
962
|
-
update_api_attr(obj, attr, value, opts.merge(attr_metadata: attr_metadata))
|
963
948
|
end
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
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
|
978
965
|
else
|
979
966
|
obj.send(setter, value.to_s)
|
980
967
|
end
|
981
|
-
|
982
|
-
|
968
|
+
rescue Exception => e
|
969
|
+
Rails.logger.warn "Error encountered assigning context parameter #{key} to " \
|
970
|
+
"'#{value}' (skipping): \"#{e.message}\")."
|
983
971
|
end
|
984
|
-
rescue Exception => e
|
985
|
-
Rails.logger.warn "Error encountered assigning context parameter #{key} to " \
|
986
|
-
"'#{value}' (skipping): \"#{e.message}\")."
|
987
972
|
end
|
988
973
|
end
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
974
|
+
|
975
|
+
def process_updated_model_save(obj, operation, opts = {})
|
976
|
+
opts = opts.dup
|
977
|
+
opts[:operation] = operation
|
978
|
+
metadata = opts.delete(:api_attr_metadata) ||
|
979
|
+
ModelApi::Utils.filtered_attrs(obj, operation, opts)
|
980
|
+
model_metadata = opts.delete(:api_model_metadata) ||
|
981
|
+
ModelApi::Utils.model_metadata(obj.class)
|
982
|
+
ModelApi::Utils.invoke_callback(model_metadata[:before_validate], obj, opts.dup)
|
983
|
+
validate_operation(obj, operation, opts)
|
984
|
+
validate_preserving_existing_errors(obj)
|
985
|
+
ModelApi::Utils.invoke_callback(model_metadata[:before_save], obj, opts.dup)
|
986
|
+
obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
|
987
|
+
successful = obj.save unless obj.errors.present?
|
988
|
+
if successful
|
989
|
+
suggested_response_status = :ok
|
990
|
+
object_errors = []
|
991
|
+
else
|
992
|
+
suggested_response_status = :bad_request
|
993
|
+
object_errors = extract_msgs_for_error(obj, opts.merge(api_attr_metadata: metadata))
|
994
|
+
unless object_errors.present?
|
995
|
+
object_errors << {
|
996
|
+
error: 'Unspecified error',
|
997
|
+
message: "Unspecified error processing #{operation}: " \
|
998
|
+
'Please contact customer service for further assistance.'
|
999
|
+
}
|
1000
|
+
end
|
1015
1001
|
end
|
1002
|
+
[suggested_response_status, object_errors]
|
1016
1003
|
end
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
message: (attr == :base ? error : "#{qualified_attr} #{error}"))
|
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
|
1042
1028
|
end
|
1043
1029
|
end
|
1030
|
+
object_errors
|
1044
1031
|
end
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
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
1060
|
processed_assoc_objects[assoc_obj] = true
|
1061
|
-
attr_prefix = "#{external_attr}
|
1061
|
+
attr_prefix = "#{external_attr}->"
|
1062
1062
|
if assoc_obj.new_record?
|
1063
1063
|
attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
|
1064
1064
|
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
@@ -1069,290 +1069,305 @@ module ModelApi
|
|
1069
1069
|
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
|
1070
1070
|
end
|
1071
1071
|
end
|
1072
|
-
|
1073
|
-
assoc_obj = obj.send(attr)
|
1074
|
-
return object_errors unless assoc_obj.present? && !processed_assoc_objects[assoc_obj]
|
1075
|
-
processed_assoc_objects[assoc_obj] = true
|
1076
|
-
attr_prefix = "#{external_attr}->"
|
1077
|
-
if assoc_obj.new_record?
|
1078
|
-
attr_metadata_create ||= ModelApi::Utils.filtered_attrs(assoc_class, :create, opts)
|
1079
|
-
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1080
|
-
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
|
1081
|
-
else
|
1082
|
-
attr_metadata_update ||= ModelApi::Utils.filtered_attrs(assoc_class, :update, opts)
|
1083
|
-
object_errors += extract_msgs_for_error(assoc_obj, opts.merge(
|
1084
|
-
attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
|
1085
|
-
end
|
1072
|
+
object_errors
|
1086
1073
|
end
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1074
|
+
|
1075
|
+
# rubocop:enable Metrics/MethodLength
|
1076
|
+
|
1077
|
+
def process_object_destroy(obj, operation, opts)
|
1078
|
+
soft_delete = obj.errors.present? ? false : object_destroy(obj, opts)
|
1090
1079
|
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
if obj.errors.blank? && (soft_delete || obj.destroyed?)
|
1095
|
-
response_status = :ok
|
1096
|
-
object_errors = []
|
1097
|
-
else
|
1098
|
-
object_errors = extract_msgs_for_error(obj, opts)
|
1099
|
-
if object_errors.present?
|
1100
|
-
response_status = :bad_request
|
1080
|
+
if obj.errors.blank? && (soft_delete || obj.destroyed?)
|
1081
|
+
response_status = :ok
|
1082
|
+
object_errors = []
|
1101
1083
|
else
|
1102
|
-
|
1103
|
-
object_errors
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1084
|
+
object_errors = extract_msgs_for_error(obj, opts)
|
1085
|
+
if object_errors.present?
|
1086
|
+
response_status = :bad_request
|
1087
|
+
else
|
1088
|
+
response_status = :internal_server_error
|
1089
|
+
object_errors << {
|
1090
|
+
error: 'Unspecified error',
|
1091
|
+
message: "Unspecified error processing #{operation}: " \
|
1092
|
+
'Please contact customer service for further assistance.'
|
1093
|
+
}
|
1094
|
+
end
|
1108
1095
|
end
|
1109
|
-
end
|
1110
|
-
|
1111
|
-
[response_status, object_errors]
|
1112
|
-
end
|
1113
1096
|
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
obj
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1097
|
+
[response_status, object_errors]
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
def object_destroy(obj, opts = {})
|
1101
|
+
klass = find_class(obj)
|
1102
|
+
object_id = obj.send(opts[:id_attribute] || :id)
|
1103
|
+
obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
|
1104
|
+
if (deleted_col = klass.columns_hash['deleted']).present?
|
1105
|
+
case deleted_col.type
|
1106
|
+
when :boolean
|
1107
|
+
obj.update_attribute(:deleted, true)
|
1108
|
+
return true
|
1109
|
+
when :integer, :decimal
|
1110
|
+
obj.update_attribute(:deleted, 1)
|
1111
|
+
return true
|
1112
|
+
else
|
1113
|
+
obj.destroy
|
1114
|
+
end
|
1126
1115
|
else
|
1127
1116
|
obj.destroy
|
1128
1117
|
end
|
1129
|
-
|
1130
|
-
|
1118
|
+
false
|
1119
|
+
rescue Exception => e
|
1120
|
+
Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
|
1121
|
+
false
|
1131
1122
|
end
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
'(setter not found): skipping.'
|
1145
|
-
add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
|
1146
|
-
return
|
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)
|
1147
1135
|
end
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
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
|
1167
1156
|
else
|
1168
|
-
|
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] }
|
1169
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]
|
1170
1214
|
else
|
1171
|
-
|
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]
|
1172
1220
|
end
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
value_array.each_with_index do |assoc_payload, index|
|
1192
|
-
opts[:ignored_fields].clear if opts.include?(:ignored_fields)
|
1193
|
-
assoc_objs << update_has_many_assoc_obj(obj, assoc, assoc_class, assoc_payload,
|
1194
|
-
opts.merge(model_metadata: model_metadata))
|
1195
|
-
if opts[:ignored_fields].present?
|
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?
|
1196
1239
|
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1197
|
-
opts[:ignored_fields] << {
|
1240
|
+
opts[:ignored_fields] << { external_attr.to_s => assoc_opts[:ignored_fields] }
|
1198
1241
|
end
|
1242
|
+
set_api_attr(parent_obj, attr, assoc_obj, opts)
|
1199
1243
|
end
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
assoc_oper
|
1210
|
-
opts[:create_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
|
1211
|
-
assoc_class, :create, opts))
|
1212
|
-
assoc_opts = opts[:create_opts]
|
1213
|
-
else
|
1214
|
-
assoc_oper = :update
|
1215
|
-
opts[:update_opts] ||= opts.merge(api_attr_metadata: ModelApi::Utils.filtered_attrs(
|
1216
|
-
assoc_class, :update, opts))
|
1217
|
-
|
1218
|
-
assoc_opts = opts[:update_opts]
|
1219
|
-
end
|
1220
|
-
if (inverse_assoc = assoc.options[:inverse_of]).present? &&
|
1221
|
-
assoc_obj.respond_to?("#{inverse_assoc}=")
|
1222
|
-
assoc_obj.send("#{inverse_assoc}=", parent_obj)
|
1223
|
-
elsif !parent_obj.new_record? && assoc_obj.respond_to?("#{assoc.foreign_key}=")
|
1224
|
-
assoc_obj.send("#{assoc.foreign_key}=", obj.id)
|
1225
|
-
end
|
1226
|
-
apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
|
1227
|
-
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
|
1228
|
-
assoc_opts.merge(operation: assoc_oper).freeze)
|
1229
|
-
assoc_obj
|
1230
|
-
end
|
1231
|
-
|
1232
|
-
def self.update_belongs_to_assoc(obj, attr, value, opts = {})
|
1233
|
-
attr_metadata = opts[:attr_metadata]
|
1234
|
-
assoc = attr_metadata[:association]
|
1235
|
-
assoc_class = assoc.class_name.constantize
|
1236
|
-
assoc_opts = opts[:ignored_fields].is_a?(Array) ? opts.merge(ignored_fields: []) : opts
|
1237
|
-
model_metadata = ModelApi::Utils.model_metadata(assoc_class)
|
1238
|
-
assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, value)
|
1239
|
-
assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
|
1240
|
-
assoc_obj ||= assoc_class.new
|
1241
|
-
obj_oper = assoc_obj.new_record? ? :create : :update
|
1242
|
-
assoc_opts = assoc_opts.merge(
|
1243
|
-
api_attr_metadata: ModelApi::Utils.filtered_attrs(assoc_class, obj_oper, opts))
|
1244
|
-
unless value.is_a?(Hash)
|
1245
|
-
obj.errors.add(attr, 'must be supplied as an object')
|
1246
|
-
return
|
1247
|
-
end
|
1248
|
-
apply_updates(assoc_obj, value, obj_oper, assoc_opts)
|
1249
|
-
ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], assoc_obj,
|
1250
|
-
opts.merge(operation: obj_oper).freeze)
|
1251
|
-
if assoc_opts[:ignored_fields].present?
|
1252
|
-
external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
|
1253
|
-
opts[:ignored_fields] << { external_attr.to_s => assoc_opts[:ignored_fields] }
|
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
1254
|
end
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
id_attributes.each do |id_attr|
|
1262
|
-
if assoc_payload.include?(id_attr.to_s)
|
1263
|
-
query = (query || assoc_class).where(id_attr => assoc_payload[id_attr.to_s])
|
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))
|
1264
1261
|
else
|
1265
|
-
|
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
|
1266
1265
|
end
|
1266
|
+
assoc_obj
|
1267
1267
|
end
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
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
|
1278
1283
|
end
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
opts = opts.frozen? ? opts : opts.dup.freeze
|
1288
|
-
on_exception.each do |klass, handler|
|
1289
|
-
klass = klass.to_s.constantize rescue nil unless klass.is_a?(Class)
|
1290
|
-
next unless klass.is_a?(Class) && e.is_a?(klass)
|
1291
|
-
if handler.respond_to?(:call)
|
1292
|
-
ModelApi::Utils.invoke_callback(handler, obj, e, opts)
|
1293
|
-
elsif handler.present?
|
1294
|
-
# Presume handler is an error message in this case
|
1295
|
-
obj.errors.add(attr_metadata[:key], handler.to_s)
|
1296
|
-
else
|
1297
|
-
add_ignored_field(opts[:ignored_fields], nil, opts[:value], attr_metadata)
|
1284
|
+
|
1285
|
+
def apply_context(query, opts = {})
|
1286
|
+
context = opts[:context]
|
1287
|
+
return query if context.nil?
|
1288
|
+
if context.respond_to?(:call)
|
1289
|
+
query = context.send(*([:call, query, opts][0..context.parameters.size]))
|
1290
|
+
elsif context.is_a?(Hash)
|
1291
|
+
context.each { |attr, value| query = query.where(attr => value) }
|
1298
1292
|
end
|
1299
|
-
|
1293
|
+
query
|
1300
1294
|
end
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
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
|
1320
1315
|
end
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
obj.errors.clear
|
1329
|
-
errors.each { |field, error| obj.errors.add(field, error) }
|
1330
|
-
else
|
1331
|
-
obj.valid?
|
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 }
|
1332
1323
|
end
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
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?
|
1347
1346
|
end
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1347
|
+
end
|
1348
|
+
|
1349
|
+
def class_or_sti_subclass(klass, req_body, operation, opts = {})
|
1350
|
+
metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
|
1351
|
+
if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
|
1352
|
+
req_body.is_a?(Hash)
|
1353
|
+
external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
|
1354
|
+
type = req_body[external_attr.to_s]
|
1355
|
+
begin
|
1356
|
+
type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
|
1357
|
+
rescue Exception => e
|
1358
|
+
Rails.logger.warn 'Error encountered parsing API input for attribute ' \
|
1359
|
+
"\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
|
1360
|
+
'using raw value instead.'
|
1361
|
+
end
|
1362
|
+
if type.present? && (type = type.camelize) != klass.name
|
1363
|
+
Rails.application.eager_load!
|
1364
|
+
klass.subclasses.each do |subclass|
|
1365
|
+
return subclass if subclass.name == type
|
1366
|
+
end
|
1352
1367
|
end
|
1353
1368
|
end
|
1369
|
+
klass
|
1354
1370
|
end
|
1355
|
-
klass
|
1356
1371
|
end
|
1357
1372
|
end
|
1358
1373
|
end
|