Skip to content

Commit d9fdcb3

Browse files
authored
Merge pull request #946 from lgebhardt/base_ops_refactor
Refactor for operations support
2 parents 9306ec0 + 53a246e commit d9fdcb3

21 files changed

Lines changed: 843 additions & 1327 deletions

lib/jsonapi-resources.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
require 'jsonapi/error'
1818
require 'jsonapi/error_codes'
1919
require 'jsonapi/request_parser'
20-
require 'jsonapi/operation_dispatcher'
2120
require 'jsonapi/processor'
2221
require 'jsonapi/relationship'
2322
require 'jsonapi/include_directives'
23+
require 'jsonapi/operation'
2424
require 'jsonapi/operation_result'
25-
require 'jsonapi/operation_results'
2625
require 'jsonapi/callbacks'
2726
require 'jsonapi/link_builder'

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 113 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ def self.included(base)
99
base.extend ClassMethods
1010
base.include Callbacks
1111
base.cattr_reader :server_error_callbacks
12-
base.define_jsonapi_resources_callbacks :process_operations
12+
base.define_jsonapi_resources_callbacks :process_operations,
13+
:transaction,
14+
:rollback
1315
end
1416

17+
attr_reader :response_document
18+
1519
def index
1620
process_request
1721
end
@@ -25,22 +29,18 @@ def show_relationship
2529
end
2630

2731
def create
28-
return unless verify_content_type_header
2932
process_request
3033
end
3134

3235
def create_relationship
33-
return unless verify_content_type_header
3436
process_request
3537
end
3638

3739
def update_relationship
38-
return unless verify_content_type_header
3940
process_request
4041
end
4142

4243
def update
43-
return unless verify_content_type_header
4444
process_request
4545
end
4646

@@ -61,50 +61,89 @@ def get_related_resources
6161
end
6262

6363
def process_request
64-
return unless verify_accept_header
64+
@response_document = create_response_document
6565

66-
@request = JSONAPI::RequestParser.new(params, context: context,
67-
key_formatter: key_formatter,
68-
server_error_callbacks: (self.class.server_error_callbacks || []))
66+
unless verify_content_type_header && verify_accept_header
67+
render_response_document
68+
return
69+
end
6970

70-
unless @request.errors.empty?
71-
render_errors(@request.errors)
72-
else
73-
operations = @request.operations
74-
unless JSONAPI.configuration.resource_cache.nil?
75-
operations.each {|op| op.options[:cache_serializer] = resource_serializer }
71+
request_parser = JSONAPI::RequestParser.new(
72+
params,
73+
context: context,
74+
key_formatter: key_formatter,
75+
server_error_callbacks: (self.class.server_error_callbacks || []))
76+
77+
transactional = request_parser.transactional?
78+
79+
force_rollback = false
80+
run_in_transaction(transactional) do
81+
begin
82+
run_callbacks :process_operations do
83+
begin
84+
request_parser.each(response_document) do |op|
85+
op.options[:serializer] = resource_serializer_klass.new(
86+
op.resource_klass,
87+
include_directives: op.options[:include_directives],
88+
fields: op.options[:fields],
89+
base_url: base_url,
90+
key_formatter: key_formatter,
91+
route_formatter: route_formatter,
92+
serialization_options: serialization_options
93+
)
94+
op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
95+
96+
process_operation(op)
97+
end
98+
rescue => e
99+
handle_exceptions(e)
100+
end
101+
end
102+
rescue => e
103+
force_rollback = true
104+
raise e
105+
ensure
106+
if response_document.has_errors? || force_rollback
107+
rollback_transaction(transactional)
108+
end
76109
end
77-
results = process_operations(operations)
78-
render_results(results)
79110
end
80-
rescue => e
81-
handle_exceptions(e)
111+
render_response_document
82112
end
83113

84-
def process_operations(operations)
85-
run_callbacks :process_operations do
86-
operation_dispatcher.process(operations)
114+
def run_in_transaction(transactional)
115+
if transactional
116+
run_callbacks :transaction do
117+
transaction do
118+
yield
119+
end
120+
end
121+
else
122+
yield
87123
end
88124
end
89125

90-
def transaction
91-
lambda { |&block|
92-
ActiveRecord::Base.transaction do
93-
block.yield
126+
def rollback_transaction(transactional)
127+
if transactional
128+
run_callbacks :rollback do
129+
rollback
94130
end
95-
}
131+
end
96132
end
97133

98-
def rollback
99-
lambda {
100-
fail ActiveRecord::Rollback
101-
}
134+
def process_operation(operation)
135+
result = operation.process
136+
response_document.add_result(result, operation)
102137
end
103138

104-
def operation_dispatcher
105-
@operation_dispatcher ||= JSONAPI::OperationDispatcher.new(transaction: transaction,
106-
rollback: rollback,
107-
server_error_callbacks: @request.server_error_callbacks)
139+
def transaction
140+
ActiveRecord::Base.transaction do
141+
yield
142+
end
143+
end
144+
145+
def rollback
146+
fail ActiveRecord::Rollback
108147
end
109148

110149
private
@@ -117,19 +156,6 @@ def resource_serializer_klass
117156
@resource_serializer_klass ||= JSONAPI::ResourceSerializer
118157
end
119158

120-
def resource_serializer
121-
@resource_serializer ||= resource_serializer_klass.new(
122-
resource_klass,
123-
include_directives: @request ? @request.include_directives : nil,
124-
fields: @request ? @request.fields : {},
125-
base_url: base_url,
126-
key_formatter: key_formatter,
127-
route_formatter: route_formatter,
128-
serialization_options: serialization_options
129-
)
130-
@resource_serializer
131-
end
132-
133159
def base_url
134160
@base_url ||= request.protocol + request.host_with_port
135161
end
@@ -139,8 +165,10 @@ def resource_klass_name
139165
end
140166

141167
def verify_content_type_header
142-
unless request.content_type == JSONAPI::MEDIA_TYPE
143-
fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
168+
if ['create', 'create_relationship', 'update_relationship', 'update'].include?(params[:action])
169+
unless request.content_type == JSONAPI::MEDIA_TYPE
170+
fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
171+
end
144172
end
145173
true
146174
rescue => e
@@ -161,13 +189,12 @@ def verify_accept_header
161189
def valid_accept_media_type?
162190
media_types = media_types_for('Accept')
163191

164-
media_types.blank? ||
165-
media_types.any? do |media_type|
166-
(media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
167-
end
192+
media_types.blank? || media_types.any? do |media_type|
193+
(media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
194+
end
168195
end
169196

170-
def media_types_for(header)
197+
def media_types_for(header)
171198
(request.headers[header] || '')
172199
.scan(MEDIA_TYPE_MATCHER)
173200
.to_a
@@ -202,79 +229,68 @@ def base_response_meta
202229
end
203230

204231
def base_meta
205-
if @request.nil? || @request.warnings.empty?
206-
base_response_meta
207-
else
208-
base_response_meta.merge(warnings: @request.warnings)
209-
end
232+
base_response_meta
210233
end
211234

212235
def base_response_links
213236
{}
214237
end
215238

216-
def render_errors(errors)
217-
operation_results = JSONAPI::OperationResults.new
218-
result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
219-
operation_results.add_result(result)
220-
221-
render_results(operation_results)
222-
end
223-
224-
def render_results(operation_results)
225-
response_doc = create_response_document(operation_results)
226-
content = response_doc.contents
239+
def render_response_document
240+
content = response_document.contents
227241

228242
render_options = {}
229-
if operation_results.has_errors?
243+
if response_document.has_errors?
230244
render_options[:json] = content
231245
else
232246
# Bypasing ActiveSupport allows us to use CompiledJson objects for cached response fragments
233247
render_options[:body] = JSON.generate(content)
234-
end
235248

236-
render_options[:location] = content[:data]["links"][:self] if (
237-
response_doc.status == :created && content[:data].class != Array
238-
)
249+
render_options[:location] = content['data']['links']['self'] if (response_document.status == 201 && content[:data].class != Array)
250+
end
239251

240252
# For whatever reason, `render` ignores :status and :content_type when :body is set.
241253
# But, we can just set those values directly in the Response object instead.
242-
response.status = response_doc.status
254+
response.status = response_document.status
243255
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
244256

245257
render(render_options)
246258
end
247259

248-
def create_response_document(operation_results)
260+
def create_response_document
249261
JSONAPI::ResponseDocument.new(
250-
operation_results,
251-
operation_results.has_errors? ? nil : resource_serializer,
252-
key_formatter: key_formatter,
253-
base_meta: base_meta,
254-
base_links: base_response_links,
255-
request: @request
262+
key_formatter: key_formatter,
263+
base_meta: base_meta,
264+
base_links: base_response_links,
265+
request: request
256266
)
257267
end
258268

259269
# override this to process other exceptions
260270
# Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
261271
def handle_exceptions(e)
262272
case e
263-
when JSONAPI::Exceptions::Error
264-
render_errors(e.errors)
265-
else
266-
if JSONAPI.configuration.exception_class_whitelisted?(e)
267-
fail e
273+
when JSONAPI::Exceptions::Error
274+
errors = e.errors
275+
when ActionController::ParameterMissing
276+
errors = JSONAPI::Exceptions::ParameterMissing.new(e.param).errors
268277
else
269-
(self.class.server_error_callbacks || []).each { |callback|
270-
safe_run_callback(callback, e)
271-
}
278+
if JSONAPI.configuration.exception_class_whitelisted?(e)
279+
fail e
280+
else
281+
if self.class.server_error_callbacks
282+
self.class.server_error_callbacks.each { |callback|
283+
safe_run_callback(callback, e)
284+
}
285+
end
272286

273-
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
274-
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
275-
render_errors(internal_server_error.errors)
276-
end
287+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
288+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
289+
errors = internal_server_error.errors
290+
end
277291
end
292+
293+
response_document.add_result(JSONAPI::ErrorsOperationResult.new(errors[0].status, errors), nil)
278294
end
279295

280296
def safe_run_callback(callback, error)
@@ -283,7 +299,7 @@ def safe_run_callback(callback, error)
283299
rescue => e
284300
Rails.logger.error { "Error in error handling callback: #{e.message} #{e.backtrace.join("\n")}" }
285301
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
286-
render_errors(internal_server_error.errors)
302+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
287303
end
288304
end
289305

lib/jsonapi/error.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ def to_hash
2424
instance_variables.each {|var| hash[var.to_s.delete('@')] = instance_variable_get(var) unless instance_variable_get(var).nil? }
2525
hash
2626
end
27+
28+
def update_with_overrides(error_object_overrides)
29+
@title = error_object_overrides[:title] || @title
30+
@detail = error_object_overrides[:detail] || @detail
31+
@id = error_object_overrides[:id] || @id
32+
@href = error_object_overrides[:href] || href
33+
34+
if error_object_overrides[:code]
35+
@code = if JSONAPI.configuration.use_text_errors
36+
TEXT_ERRORS[error_object_overrides[:code]]
37+
else
38+
error_object_overrides[:code]
39+
end
40+
end
41+
42+
@source = error_object_overrides[:source] || @source
43+
@links = error_object_overrides[:links] || @links
44+
45+
if error_object_overrides[:status]
46+
@status = Rack::Utils::SYMBOL_TO_STATUS_CODE[error_object_overrides[:status]].to_s
47+
end
48+
@meta = error_object_overrides[:meta] || @meta
49+
end
2750
end
2851

2952
class Warning

0 commit comments

Comments
 (0)