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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 14b9b0dc45ee29320fa640f6b209b5481b5f75b7
4
- data.tar.gz: 9f693375afd6e922b43eaaad1fc0f02ae6f5908e
3
+ metadata.gz: 3ad27c0bbc9961da1c6b1aa4715e05f3f6af9297
4
+ data.tar.gz: a81ac47433b86382e72f21ae7e12113dabc3fcd5
5
5
  SHA512:
6
- metadata.gz: f7c8fbbe1fd448f4abfa12b38707b7e15d018a025f2456b7d52cde0ac81212eeff4a3039e9a677386cb3d61795ca89ebdf2c43c80d49f58ab7adfc7e75487c31
7
- data.tar.gz: 350cd78aae1b10b904b187b06fbcefb69e1ef57adb306ef8273ea3545fce57704ec0fde053562d4828f6755dd8688e9e08ad585f4d5f8456ca3d539ef96e3e1e
6
+ metadata.gz: 4b99fcc0cffd7e8d24210afab23dea9b8cec64bef71afea263738ca6e4b4169232b7792fa5f2fd33471d610cc4d406f6499724ba06e88f8aacf337e671b98dee
7
+ data.tar.gz: 823d85c47661c643d443f8bef2403ce22ee3f20f2a53ce6fbfe45630271a0005d76a411569573bf10d3b9dcf655d02a45d2a86b02eda11a0406f8a086ed31871
data/README.md CHANGED
@@ -1,2 +1,199 @@
1
1
  # model_api
2
- Ruby gem allowing Ruby on Rails developers to create REST APIs using metadata defined inside their ActiveRecord models.
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 &lt;access token&gt;")',
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
- def self.included(base)
18
- base.extend(ClassMethods)
19
-
20
- base.send(:include, InstanceMethods)
21
-
22
- base.send(:before_filter, :common_headers)
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.send(opts[:user_id_attribute] || :id)
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
- return true if current_user.try(:admin_api_user?)
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. Please contact customer ' \
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
- if (user_tz = current_user.try(:preference).try(:time_zone)).present?
795
- time_zone = ActiveSupport::TimeZone.new(user_tz)
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
- def self.find_class(obj, opts = {})
921
- return nil if obj.nil?
922
- opts[:class] || (obj.respond_to?(:klass) ? obj.klass : obj.class)
923
- end
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
- collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1
930
- collection_links[:first] = [coll_route, { page: 1 }]
931
- collection_links[:last] = [coll_route, { page: last_page }]
932
- end
933
-
934
- def self.object_from_req_body(root_elem, req_body, format)
935
- if format == :json
936
- request_obj = req_body
937
- else
938
- request_obj = req_body[root_elem]
939
- if request_obj.blank?
940
- request_obj = req_body['obj']
941
- if request_obj.blank? && req_body.size == 1
942
- request_obj = req_body.values.first
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
- fail 'Invalid request format' unless request_obj.present?
947
- request_obj
948
- end
949
-
950
- def self.apply_updates(obj, req_obj, operation, opts = {})
951
- opts = opts.merge(object: opts[:object] || obj)
952
- metadata = ModelApi::Utils.filtered_ext_attrs(opts[:api_attr_metadata] ||
953
- ModelApi::Utils.filtered_attrs(obj, operation, opts), operation, opts)
954
- set_context_attrs(obj, opts)
955
- req_obj.each do |attr, value|
956
- attr = attr.to_sym
957
- attr_metadata = metadata[attr]
958
- unless attr_metadata.present?
959
- add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
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
- end
965
-
966
- def self.set_context_attrs(obj, opts = {})
967
- klass = (obj.class < ActiveRecord::Base ? obj.class : nil)
968
- (opts[:context] || {}).each do |key, value|
969
- begin
970
- setter = "#{key}="
971
- next unless obj.respond_to?(setter)
972
- if (column = klass.try(:columns_hash).try(:[], key.to_s)).present?
973
- case column.type
974
- when :integer, :primary_key then
975
- obj.send("#{key}=", value.to_i)
976
- when :decimal, :float then
977
- obj.send("#{key}=", value.to_f)
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
- else
982
- obj.send(setter, value.to_s)
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
- end
990
-
991
- def self.process_updated_model_save(obj, operation, opts = {})
992
- opts = opts.dup
993
- opts[:operation] = operation
994
- metadata = opts.delete(:api_attr_metadata) ||
995
- ModelApi::Utils.filtered_attrs(obj, operation, opts)
996
- model_metadata = opts.delete(:api_model_metadata) || ModelApi::Utils.model_metadata(obj.class)
997
- ModelApi::Utils.invoke_callback(model_metadata[:before_validate], obj, opts.dup)
998
- validate_operation(obj, operation, opts)
999
- validate_preserving_existing_errors(obj)
1000
- ModelApi::Utils.invoke_callback(model_metadata[:before_save], obj, opts.dup)
1001
- obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
1002
- successful = obj.save unless obj.errors.present?
1003
- if successful
1004
- suggested_response_status = :ok
1005
- object_errors = []
1006
- else
1007
- suggested_response_status = :bad_request
1008
- object_errors = extract_msgs_for_error(obj, opts.merge(api_attr_metadata: metadata))
1009
- unless object_errors.present?
1010
- object_errors << {
1011
- error: 'Unspecified error',
1012
- message: "Unspecified error processing #{operation}: " \
1013
- 'Please contact customer service for further assistance.'
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
- [suggested_response_status, object_errors]
1018
- end
1019
-
1020
- def self.extract_msgs_for_error(obj, opts = {})
1021
- object_errors = []
1022
- attr_prefix = opts[:attr_prefix] || ''
1023
- api_metadata = opts[:api_attr_metadata] || ModelApi::Utils.api_attrs(obj.class)
1024
- obj.errors.each do |attr, attr_errors|
1025
- attr_errors = [attr_errors] unless attr_errors.is_a?(Array)
1026
- attr_errors.each do |error|
1027
- attr_metadata = api_metadata[attr] || {}
1028
- qualified_attr = "#{attr_prefix}#{ModelApi::Utils.ext_attr(attr, attr_metadata)}"
1029
- assoc_errors = nil
1030
- if attr_metadata[:type] == :association
1031
- assoc_errors = extract_assoc_error_msgs(obj, attr, opts.merge(
1032
- attr_metadata: attr_metadata))
1033
- end
1034
- if assoc_errors.present?
1035
- object_errors += assoc_errors
1036
- else
1037
- error_hash = {}
1038
- error_hash[:object] = attr_prefix if attr_prefix.present?
1039
- error_hash[:attribute] = qualified_attr unless attr == :base
1040
- object_errors << error_hash.merge(error: error,
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
- object_errors
1046
- end
1047
-
1048
- # rubocop:disable Metrics/MethodLength
1049
- def self.extract_assoc_error_msgs(obj, attr, opts)
1050
- object_errors = []
1051
- attr_metadata = opts[:attr_metadata] || {}
1052
- processed_assoc_objects = {}
1053
- assoc = attr_metadata[:association]
1054
- assoc_class = assoc.class_name.constantize
1055
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1056
- attr_metadata_create = attr_metadata_update = nil
1057
- if assoc.macro == :has_many
1058
- obj.send(attr).each_with_index do |assoc_obj, index|
1059
- next if processed_assoc_objects[assoc_obj]
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}[#{index}]."
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
- else
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
- object_errors
1088
- end
1089
- # rubocop:enable Metrics/MethodLength
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
- def self.process_object_destroy(obj, operation, opts)
1092
- soft_delete = obj.errors.present? ? false : object_destroy(obj, opts)
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
- response_status = :internal_server_error
1103
- object_errors << {
1104
- error: 'Unspecified error',
1105
- message: "Unspecified error processing #{operation}: " \
1106
- 'Please contact customer service for further assistance.'
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
- def self.object_destroy(obj, opts = {})
1115
- klass = find_class(obj)
1116
- object_id = obj.send(opts[:id_attribute] || :id)
1117
- obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
1118
- if (deleted_col = klass.columns_hash['deleted']).present?
1119
- case deleted_col.type
1120
- when :boolean
1121
- obj.update_attribute(:deleted, true)
1122
- return true
1123
- when :integer, :decimal
1124
- obj.update_attribute(:deleted, 1)
1125
- return true
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
- else
1130
- obj.destroy
1118
+ false
1119
+ rescue Exception => e
1120
+ Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
1121
+ false
1131
1122
  end
1132
- false
1133
- rescue Exception => e
1134
- Rails.logger.warn "Error destroying #{klass.name} \"#{object_id}\": \"#{e.message}\")."
1135
- false
1136
- end
1137
-
1138
- def self.set_api_attr(obj, attr, value, opts)
1139
- attr_metadata = opts[:attr_metadata]
1140
- internal_field = attr_metadata[:key] || attr
1141
- setter = attr_metadata[:setter] || "#{(internal_field)}="
1142
- unless obj.respond_to?(setter)
1143
- Rails.logger.warn "Error encountered assigning API input for attribute \"#{attr}\" " \
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
- obj.send(setter, value)
1149
- end
1150
-
1151
- def self.update_api_attr(obj, attr, value, opts = {})
1152
- attr_metadata = opts[:attr_metadata]
1153
- begin
1154
- value = ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)
1155
- rescue Exception => e
1156
- Rails.logger.warn "Error encountered parsing API input for attribute \"#{attr}\" " \
1157
- "(\"#{e.message}\"): \"#{value.to_s.first(1000)}\" ... using raw value instead."
1158
- end
1159
- begin
1160
- if attr_metadata[:type] == :association && attr_metadata[:parse].blank?
1161
- attr_metadata = opts[:attr_metadata]
1162
- assoc = attr_metadata[:association]
1163
- if assoc.macro == :has_many
1164
- update_has_many_assoc(obj, attr, value, opts)
1165
- elsif assoc.macro == :belongs_to
1166
- update_belongs_to_assoc(obj, attr, value, opts)
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
- add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
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
- set_api_attr(obj, attr, value, opts)
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
- rescue Exception => e
1174
- handle_api_setter_exception(e, obj, attr_metadata, opts)
1175
- end
1176
- end
1177
-
1178
- def self.update_has_many_assoc(obj, attr, value, opts = {})
1179
- attr_metadata = opts[:attr_metadata]
1180
- assoc = attr_metadata[:association]
1181
- assoc_class = assoc.class_name.constantize
1182
- model_metadata = ModelApi::Utils.model_metadata(assoc_class)
1183
- value_array = value.to_a rescue nil
1184
- unless value_array.is_a?(Array)
1185
- obj.errors.add(attr, 'must be supplied as an array of objects')
1186
- return
1187
- end
1188
- opts = opts.merge(model_metadata: model_metadata)
1189
- opts[:ignored_fields] = [] if opts.include?(:ignored_fields)
1190
- assoc_objs = []
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] << { "#{external_attr}[#{index}]" => 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
- set_api_attr(obj, attr, assoc_objs, opts)
1201
- end
1202
-
1203
- def self.update_has_many_assoc_obj(parent_obj, assoc, assoc_class, assoc_payload, opts = {})
1204
- model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(assoc_class)
1205
- assoc_obj = find_by_id_attrs(model_metadata[:id_attributes], assoc_class, assoc_payload)
1206
- assoc_obj = assoc_obj.first unless assoc_obj.nil? || assoc_obj.count != 1
1207
- assoc_obj ||= assoc_class.new
1208
- if assoc_obj.new_record?
1209
- assoc_oper = :create
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
- set_api_attr(obj, attr, assoc_obj, opts)
1256
- end
1257
-
1258
- def self.find_by_id_attrs(id_attributes, assoc_class, assoc_payload)
1259
- return nil unless id_attributes.present?
1260
- query = nil
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
- return nil
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
- query
1269
- end
1270
-
1271
- def self.apply_context(query, opts = {})
1272
- context = opts[:context]
1273
- return query if context.nil?
1274
- if context.respond_to?(:call)
1275
- query = context.send(*([:call, query, opts][0..context.parameters.size]))
1276
- elsif context.is_a?(Hash)
1277
- context.each { |attr, value| query = query.where(attr => value) }
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
- query
1280
- end
1281
-
1282
- def self.handle_api_setter_exception(e, obj, attr_metadata, opts = {})
1283
- return unless attr_metadata.is_a?(Hash)
1284
- on_exception = attr_metadata[:on_exception]
1285
- fail e unless on_exception.present?
1286
- on_exception = { Exception => on_exception } unless on_exception.is_a?(Hash)
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
- break
1293
+ query
1300
1294
  end
1301
- end
1302
-
1303
- def self.add_ignored_field(ignored_fields, attr, value, attr_metadata)
1304
- return unless ignored_fields.is_a?(Array)
1305
- attr_metadata ||= {}
1306
- external_attr = ModelApi::Utils.ext_attr(attr, attr_metadata)
1307
- return unless external_attr.present?
1308
- ignored_fields << { external_attr => value }
1309
- end
1310
-
1311
- def self.validate_operation(obj, operation, opts = {})
1312
- klass = find_class(obj, opts)
1313
- model_metadata = opts[:api_model_metadata] || ModelApi::Utils.model_metadata(klass)
1314
- return nil unless operation.present?
1315
- opts = opts.frozen? ? opts : opts.dup.freeze
1316
- if obj.nil?
1317
- ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], opts)
1318
- else
1319
- ModelApi::Utils.invoke_callback(model_metadata[:"validate_#{operation}"], obj, opts)
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
- end
1322
-
1323
- def self.validate_preserving_existing_errors(obj)
1324
- if obj.errors.present?
1325
- errors = obj.errors.messages.dup
1326
- obj.valid?
1327
- errors = obj.errors.messages.merge(errors)
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
- end
1334
-
1335
- def self.class_or_sti_subclass(klass, req_body, operation, opts = {})
1336
- metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
1337
- if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
1338
- req_body.is_a?(Hash)
1339
- external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
1340
- type = req_body[external_attr.to_s]
1341
- begin
1342
- type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
1343
- rescue Exception => e
1344
- Rails.logger.warn 'Error encountered parsing API input for attribute ' \
1345
- "\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
1346
- 'using raw value instead.'
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
- if type.present? && (type = type.camelize) != klass.name
1349
- Rails.application.eager_load!
1350
- klass.subclasses.each do |subclass|
1351
- return subclass if subclass.name == type
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