Skip to content

Commit 1d4572d

Browse files
committed
Merge pull request #235 from cerebris/op_response_codes
Allow alternate response code
2 parents 783ec13 + 043791f commit 1d4572d

7 files changed

Lines changed: 134 additions & 32 deletions

File tree

lib/jsonapi/error_codes.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module JSONAPI
1919
INVALID_PAGE_VALUE = 118
2020
INVALID_FIELD_FORMAT = 119
2121
INVALID_FILTERS_SYNTAX = 120
22+
SAVE_FAILED = 121
2223
FORBIDDEN = 403
2324
RECORD_NOT_FOUND = 404
2425
UNSUPPORTED_MEDIA_TYPE = 415
@@ -44,9 +45,11 @@ module JSONAPI
4445
INVALID_PAGE_OBJECT => 'INVALID_PAGE_OBJECT',
4546
INVALID_PAGE_VALUE => 'INVALID_PAGE_VALUE',
4647
INVALID_FIELD_FORMAT => 'INVALID_FIELD_FORMAT',
48+
INVALID_FILTERS_SYNTAX => 'INVALID_FILTERS_SYNTAX',
49+
SAVE_FAILED => 'SAVE_FAILED',
4750
FORBIDDEN => 'FORBIDDEN',
4851
RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
4952
UNSUPPORTED_MEDIA_TYPE => 'UNSUPPORTED_MEDIA_TYPE',
50-
LOCKED => 'LOCKED',
51-
INVALID_FILTERS_SYNTAX => 'INVALID_FILTERS_SYNTAX' }
53+
LOCKED => 'LOCKED'
54+
}
5255
end

lib/jsonapi/exceptions.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@ def errors
305305
end
306306
end
307307

308+
class SaveFailed < Error
309+
def errors
310+
[JSONAPI::Error.new(code: JSONAPI::SAVE_FAILED,
311+
status: :unprocessable_entity,
312+
title: 'Save failed or was cancelled',
313+
detail: 'Save failed or was cancelled')]
314+
end
315+
end
316+
308317
class InvalidPageObject < Error
309318
def errors
310319
[JSONAPI::Error.new(code: JSONAPI::INVALID_PAGE_OBJECT,

lib/jsonapi/operation.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ def initialize(resource_klass, options = {})
147147

148148
def apply(context)
149149
resource = @resource_klass.create(context)
150-
resource.replace_fields(@data)
150+
result = resource.replace_fields(@data)
151151

152-
return JSONAPI::ResourceOperationResult.new(:created, resource)
152+
return JSONAPI::ResourceOperationResult.new((result == :completed ? :created : :accepted), resource)
153153

154154
rescue JSONAPI::Exceptions::Error => e
155155
return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
@@ -165,9 +165,9 @@ def initialize(resource_klass, options = {})
165165

166166
def apply(context)
167167
resource = @resource_klass.find_by_key(@resource_id, context: context)
168-
resource.remove
168+
result = resource.remove
169169

170-
return JSONAPI::OperationResult.new(:no_content)
170+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
171171

172172
rescue JSONAPI::Exceptions::Error => e
173173
return JSONAPI::ErrorsOperationResult.new(e.errors[0].code, e.errors)
@@ -185,9 +185,9 @@ def initialize(resource_klass, options = {})
185185

186186
def apply(context)
187187
resource = @resource_klass.find_by_key(@resource_id, context: context)
188-
resource.replace_fields(data)
188+
result = resource.replace_fields(data)
189189

190-
return JSONAPI::ResourceOperationResult.new(:ok, resource)
190+
return JSONAPI::ResourceOperationResult.new(result == :completed ? :ok : :accepted, resource)
191191
end
192192
end
193193

@@ -203,9 +203,9 @@ def initialize(resource_klass, options = {})
203203

204204
def apply(context)
205205
resource = @resource_klass.find_by_key(@resource_id, context: context)
206-
resource.replace_has_one_link(@association_type, @key_value)
206+
result = resource.replace_has_one_link(@association_type, @key_value)
207207

208-
return JSONAPI::OperationResult.new(:no_content)
208+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
209209
end
210210
end
211211

@@ -221,9 +221,9 @@ def initialize(resource_klass, options)
221221

222222
def apply(context)
223223
resource = @resource_klass.find_by_key(@resource_id, context: context)
224-
resource.create_has_many_links(@association_type, @data)
224+
result = resource.create_has_many_links(@association_type, @data)
225225

226-
return JSONAPI::OperationResult.new(:no_content)
226+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
227227
end
228228
end
229229

@@ -239,9 +239,9 @@ def initialize(resource_klass, options)
239239

240240
def apply(context)
241241
resource = @resource_klass.find_by_key(@resource_id, context: context)
242-
resource.replace_has_many_links(@association_type, @data)
242+
result = resource.replace_has_many_links(@association_type, @data)
243243

244-
return JSONAPI::OperationResult.new(:no_content)
244+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
245245
end
246246
end
247247

@@ -257,9 +257,9 @@ def initialize(resource_klass, options)
257257

258258
def apply(context)
259259
resource = @resource_klass.find_by_key(@resource_id, context: context)
260-
resource.remove_has_many_link(@association_type, @associated_key)
260+
result = resource.remove_has_many_link(@association_type, @associated_key)
261261

262-
return JSONAPI::OperationResult.new(:no_content)
262+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
263263
end
264264
end
265265

@@ -274,9 +274,9 @@ def initialize(resource_klass, options)
274274

275275
def apply(context)
276276
resource = @resource_klass.find_by_key(@resource_id, context: context)
277-
resource.remove_has_one_link(@association_type)
277+
result = resource.remove_has_one_link(@association_type)
278278

279-
return JSONAPI::OperationResult.new(:no_content)
279+
return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
280280
end
281281
end
282282
end

lib/jsonapi/resource.rb

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,26 @@ def is_new?
3535
end
3636

3737
def change(callback)
38+
completed = false
39+
3840
if @changing
3941
run_callbacks callback do
40-
yield
42+
completed = (yield == :completed)
4143
end
4244
else
4345
run_callbacks is_new? ? :create : :update do
4446
@changing = true
4547
run_callbacks callback do
46-
yield
48+
completed = (yield == :completed)
4749
end
4850

49-
save if @save_needed || is_new?
51+
if @save_needed || is_new?
52+
completed = (save == :completed)
53+
end
5054
end
5155
end
56+
57+
return completed ? :completed : :accepted
5258
end
5359

5460
def remove
@@ -111,18 +117,42 @@ def save
111117
end
112118
end
113119

120+
# Override this on a resource to return a different result code. Any
121+
# value other than :completed will result in operations returning
122+
# `:accepted`
123+
#
124+
# For example to return `:accepted` if your model does not immediately
125+
# save resources to the database you could override `_save` as follows:
126+
#
127+
# ```
128+
# def _save
129+
# super
130+
# return :accepted
131+
# end
132+
# ```
114133
def _save
115134
unless @model.valid?
116135
raise JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
117136
end
118137

119-
saved = @model.save
138+
if defined? @model.save
139+
saved = @model.save
140+
unless saved
141+
raise JSONAPI::Exceptions::SaveFailed.new
142+
end
143+
else
144+
saved = true
145+
end
146+
120147
@save_needed = !saved
121-
saved
148+
149+
return :completed
122150
end
123151

124152
def _remove
125153
@model.destroy
154+
155+
return :completed
126156
end
127157

128158
def _create_has_many_links(association_type, association_key_values)
@@ -139,33 +169,43 @@ def _create_has_many_links(association_type, association_key_values)
139169
raise JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
140170
end
141171
end
172+
173+
return :completed
142174
end
143175

144176
def _replace_has_many_links(association_type, association_key_values)
145177
association = self.class._associations[association_type]
146178

147179
send("#{association.foreign_key}=", association_key_values)
148180
@save_needed = true
181+
182+
return :completed
149183
end
150184

151185
def _replace_has_one_link(association_type, association_key_value)
152186
association = self.class._associations[association_type]
153187

154188
send("#{association.foreign_key}=", association_key_value)
155189
@save_needed = true
190+
191+
return :completed
156192
end
157193

158194
def _remove_has_many_link(association_type, key)
159195
association = self.class._associations[association_type]
160196

161197
@model.send(association.type).delete(key)
198+
199+
return :completed
162200
end
163201

164202
def _remove_has_one_link(association_type)
165203
association = self.class._associations[association_type]
166204

167205
send("#{association.foreign_key}=", nil)
168206
@save_needed = true
207+
208+
return :completed
169209
end
170210

171211
def _replace_fields(field_data)
@@ -191,6 +231,8 @@ def _replace_fields(field_data)
191231
field_data[:has_many].each do |association_type, values|
192232
replace_has_many_links(association_type, values)
193233
end if field_data[:has_many]
234+
235+
return :completed
194236
end
195237

196238
class << self

test/controllers/controller_test.rb

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,7 +1983,7 @@ def test_poro_create_simple
19831983
}
19841984
}
19851985

1986-
assert_response :created
1986+
assert_response :accepted
19871987
assert json_response['data'].is_a?(Hash)
19881988
assert_equal 'Tabby', json_response['data']['attributes']['name']
19891989
end
@@ -2017,7 +2017,7 @@ def test_poro_create_update
20172017
}
20182018
}
20192019

2020-
assert_response :created
2020+
assert_response :accepted
20212021
assert json_response['data'].is_a?(Hash)
20222022
assert_equal 'Calic', json_response['data']['attributes']['name']
20232023

@@ -2322,3 +2322,40 @@ def test_index_default_filter_override
23222322
assert_equal 4, json_response['data'].size
23232323
end
23242324
end
2325+
2326+
class Api::V1::PlanetsControllerTest < ActionController::TestCase
2327+
def test_save_model_callbacks
2328+
set_content_type_header!
2329+
post :create,
2330+
{
2331+
data: {
2332+
type: 'planets',
2333+
attributes: {
2334+
name: 'Zeus',
2335+
description: 'The largest planet in the solar system. Discovered in 2015.'
2336+
}
2337+
}
2338+
}
2339+
2340+
assert_response :created
2341+
assert json_response['data'].is_a?(Hash)
2342+
assert_equal 'Zeus', json_response['data']['attributes']['name']
2343+
end
2344+
2345+
def test_save_model_callbacks_fail
2346+
set_content_type_header!
2347+
post :create,
2348+
{
2349+
data: {
2350+
type: 'planets',
2351+
attributes: {
2352+
name: 'Pluto',
2353+
description: 'Yes, it is a planet.'
2354+
}
2355+
}
2356+
}
2357+
2358+
assert_response :unprocessable_entity
2359+
assert_match /Save failed or was cancelled/, json_response['errors'][0]['detail']
2360+
end
2361+
end

test/fixtures/active_record.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ class Planet < ActiveRecord::Base
222222
belongs_to :planet_type
223223

224224
has_and_belongs_to_many :tags, join_table: :planets_tags
225+
226+
# Test model callback cancelling save
227+
before_save :check_not_pluto
228+
229+
def check_not_pluto
230+
# Pluto can't be a planet, so cancel the save
231+
if name.downcase == 'pluto'
232+
return false
233+
end
234+
end
225235
end
226236

227237
class PlanetType < ActiveRecord::Base
@@ -262,10 +272,6 @@ def destroy
262272
$breed_data.remove(@id)
263273
end
264274

265-
def save
266-
true
267-
end
268-
269275
def valid?
270276
@errors.clear
271277
if name.is_a?(String) && name.length > 0
@@ -701,6 +707,11 @@ def self.find(filters, options = {})
701707
def self.find_by_key(id, options = {})
702708
BreedResource.new($breed_data.breeds[id.to_i], options[:context])
703709
end
710+
711+
def _save
712+
super
713+
return :accepted
714+
end
704715
end
705716

706717
class PlanetResource < JSONAPI::Resource
@@ -968,7 +979,7 @@ class BadlyNamedAttributesResource < JSONAPI::Resource
968979
description: 'Saturn is the sixth planet from the Sun and the second largest planet in the Solar System, after Jupiter.',
969980
planet_type_id: planetoid.id)
970981
titan = Moon.create(name:'Titan', description: 'Best known of the Saturn moons.', planet_id: saturn.id)
971-
pluto = Planet.create(name: 'Pluto', description: 'Pluto is the smallest planet.', planet_type_id: planetoid.id)
982+
makemake = Planet.create(name: 'Makemake', description: 'A small planetoid in the Kuiperbelt.', planet_type_id: planetoid.id)
972983
uranus = Planet.create(name: 'Uranus', description: 'Insert adolescent jokes here.', planet_type_id: gas_giant.id)
973984
jupiter = Planet.create(name: 'Jupiter', description: 'A gas giant.', planet_type_id: gas_giant.id)
974985
betax = Planet.create(name: 'Beta X', description: 'Newly discovered Planet X', planet_type_id: unknown.id)

test/unit/operation/operations_processor_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,8 @@ def test_remove_resource
304304
op = JSONAPI::OperationsProcessor.new
305305

306306
count = Planet.count
307-
pluto = Planet.find(2)
308-
assert_equal(pluto.name, 'Pluto')
307+
makemake = Planet.find(2)
308+
assert_equal(makemake.name, 'Makemake')
309309

310310
operations = [
311311
JSONAPI::RemoveResourceOperation.new(PlanetResource, resource_id: 2),

0 commit comments

Comments
 (0)